Compare commits

..

No commits in common. "master" and "1.5.1.2" have entirely different histories.

20 changed files with 47 additions and 427 deletions

@ -1 +1 @@
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
Subproject commit f354444776591ae423e2d8374aae346308d81424

@ -1 +1 @@
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa

@ -1 +1 @@
Subproject commit d889f9ef918514a46049725052d378b441915b00
Subproject commit 27893a85adb57a301dd93fd2c7d318bfd4c12a0f

View file

@ -17,7 +17,7 @@ public class PenumbraApi(
UiApi ui) : IDisposable, IApiService, IPenumbraApi
{
public const int BreakingVersion = 5;
public const int FeatureVersion = 13;
public const int FeatureVersion = 12;
public void Dispose()
{

View file

@ -2,14 +2,11 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Interop;
using Penumbra.Interop.Services;
namespace Penumbra.Api.Api;
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
{
@ -31,24 +28,6 @@ public class RedrawApi(RedrawService redrawService, IFramework framework, Collec
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
}
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
{
if (!collections.Storage.ById(collectionId, out var collection))
collection = ModCollection.Empty;
framework.RunOnFrameworkThread(() =>
{
foreach (var actor in objects.Objects)
{
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
if (collection == modCollection)
{
redrawService.RedrawObject(actor.ObjectIndex, setting);
}
}
});
}
public event GameObjectRedrawnDelegate? GameObjectRedrawn
{
add => redrawService.GameObjectRedrawn += value;

View file

@ -5,7 +5,6 @@ using EmbedIO.WebApi;
using OtterGui.Services;
using Penumbra.Api.Api;
using Penumbra.Api.Enums;
using Penumbra.Mods.Settings;
namespace Penumbra.Api;
@ -14,15 +13,13 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller : WebApiController
{
// @formatter:off
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
// @formatter:on
}
@ -68,12 +65,6 @@ public class HttpApi : IDisposable, IApiService
private partial class Controller(IPenumbraApi api, IFramework framework)
{
public partial string GetModDirectory()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
return api.PluginState.GetModDirectory();
}
public partial object? GetMods()
{
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
@ -125,7 +116,6 @@ public class HttpApi : IDisposable, IApiService
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
}
public async partial Task FocusMod()
{
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
@ -134,30 +124,6 @@ public class HttpApi : IDisposable, IApiService
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
}
public async partial Task SetModSettings()
{
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
await framework.RunOnFrameworkThread(() =>
{
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
if (data.Inherit.HasValue)
{
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
if (data.Inherit.Value)
return;
}
if (data.State.HasValue)
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
if (data.Priority.HasValue)
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
foreach (var (group, settings) in data.Settings ?? [])
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
}
).ConfigureAwait(false);
}
private record ModReloadData(string Path, string Name)
{
public ModReloadData()
@ -185,19 +151,5 @@ public class HttpApi : IDisposable, IApiService
: this(string.Empty, RedrawType.Redraw, -1)
{ }
}
private record SetModSettingsData(
Guid? CollectionId,
string ModPath,
string ModName,
bool? Inherit,
bool? State,
int? Priority,
Dictionary<string, List<string>>? Settings)
{
public SetModSettingsData()
: this(null, string.Empty, string.Empty, null, null, null, null)
{}
}
}
}

View file

@ -88,7 +88,6 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),

View file

@ -121,10 +121,6 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
}).ToArray();
ImGui.OpenPopup("Changed Item List");
}
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
if (ImGui.Button("Redraw##ObjectCollection"))
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
}
private void DrawChangedItemPopup()

View file

@ -53,7 +53,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty;
public string WatchDirectory { get; set; } = string.Empty;
public bool? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false;
@ -77,8 +76,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false;
public bool EnableDirectoryWatch { get; set; } = false;
public bool EnableAutomaticModImport { get; set; } = false;
public bool EnableCustomShapes { get; set; } = true;
public PcpSettings PcpSettings = new();
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;

View file

@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver(
{
var item = charaEntry.Value;
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
Penumbra.Log.Excessive(
Penumbra.Log.Verbose(
$"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}.");
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
{

View file

@ -75,7 +75,6 @@ public sealed class CutsceneService : IRequiredService, IDisposable
return false;
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
_objects.InvokeRequiredUpdates();
return true;
}

View file

@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable
return;
foreach (ref var f in currentTerritory->FurnitureManager.FurnitureMemory)
foreach (ref var f in currentTerritory->Furniture)
{
var gameObject = f.Index >= 0 ? currentTerritory->FurnitureManager.ObjectManager.ObjectArray.Objects[f.Index].Value : null;
var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null;
if (gameObject == null)
continue;

View file

@ -21,6 +21,7 @@ using Penumbra.UI;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Penumbra.GameData;
using Penumbra.GameData.Data;
using Penumbra.Interop;
using Penumbra.Interop.Hooks;

View file

@ -1,209 +0,0 @@
using OtterGui.Services;
using Penumbra.Mods.Manager;
namespace Penumbra.Services;
public class FileWatcher : IDisposable, IService
{
// TODO: use ConcurrentSet when it supports comparers in Luna.
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
private readonly ModImportManager _modImportManager;
private readonly MessageService _messageService;
private readonly Configuration _config;
private bool _pausedConsumer;
private FileSystemWatcher? _fsw;
private CancellationTokenSource? _cts = new();
private Task? _consumer;
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
{
_modImportManager = modImportManager;
_messageService = messageService;
_config = config;
if (_config.EnableDirectoryWatch)
{
SetupFileWatcher(_config.WatchDirectory);
SetupConsumerTask();
}
}
public void Toggle(bool value)
{
if (_config.EnableDirectoryWatch == value)
return;
_config.EnableDirectoryWatch = value;
_config.Save();
if (value)
{
SetupFileWatcher(_config.WatchDirectory);
SetupConsumerTask();
}
else
{
EndFileWatcher();
EndConsumerTask();
}
}
internal void PauseConsumer(bool pause)
=> _pausedConsumer = pause;
private void EndFileWatcher()
{
if (_fsw is null)
return;
_fsw.Dispose();
_fsw = null;
}
private void SetupFileWatcher(string directory)
{
EndFileWatcher();
_fsw = new FileSystemWatcher
{
IncludeSubdirectories = false,
NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime,
InternalBufferSize = 32 * 1024,
};
// Only wake us for the exact patterns we care about
_fsw.Filters.Add("*.pmp");
_fsw.Filters.Add("*.pcp");
_fsw.Filters.Add("*.ttmp");
_fsw.Filters.Add("*.ttmp2");
_fsw.Created += OnPath;
_fsw.Renamed += OnPath;
UpdateDirectory(directory);
}
private void EndConsumerTask()
{
if (_cts is not null)
{
_cts.Cancel();
_cts = null;
}
_consumer = null;
}
private void SetupConsumerTask()
{
EndConsumerTask();
_cts = new CancellationTokenSource();
_consumer = Task.Factory.StartNew(
() => ConsumerLoopAsync(_cts.Token),
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
}
public void UpdateDirectory(string newPath)
{
if (_config.WatchDirectory != newPath)
{
_config.WatchDirectory = newPath;
_config.Save();
}
if (_fsw is null)
return;
_fsw.EnableRaisingEvents = false;
if (!Directory.Exists(newPath) || newPath.Length is 0)
{
_fsw.Path = string.Empty;
}
else
{
_fsw.Path = newPath;
_fsw.EnableRaisingEvents = true;
}
}
private void OnPath(object? sender, FileSystemEventArgs e)
=> _pending.TryAdd(e.FullPath, 0);
private async Task ConsumerLoopAsync(CancellationToken token)
{
while (true)
{
var (path, _) = _pending.FirstOrDefault();
if (path is null || _pausedConsumer)
{
await Task.Delay(500, token).ConfigureAwait(false);
continue;
}
try
{
await ProcessOneAsync(path, token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Penumbra.Log.Debug("[FileWatcher] Canceled via Token.");
}
catch (Exception ex)
{
Penumbra.Log.Warning($"[FileWatcher] Error during Processing: {ex}");
}
finally
{
_pending.TryRemove(path, out _);
}
}
}
private async Task ProcessOneAsync(string path, CancellationToken token)
{
// Downloads often finish via rename; file may be locked briefly.
// Wait until it exists and is readable; also require two stable size checks.
const int maxTries = 40;
long lastLen = -1;
for (var i = 0; i < maxTries && !token.IsCancellationRequested; i++)
{
if (!File.Exists(path))
{
await Task.Delay(100, token);
continue;
}
try
{
var fi = new FileInfo(path);
var len = fi.Length;
if (len > 0 && len == lastLen)
{
if (_config.EnableAutomaticModImport)
_modImportManager.AddUnpack(path);
else
_messageService.AddMessage(new InstallNotification(_modImportManager, path), false);
return;
}
lastLen = len;
}
catch (IOException)
{
Penumbra.Log.Debug($"[FileWatcher] File is still being written to.");
}
catch (UnauthorizedAccessException)
{
Penumbra.Log.Debug($"[FileWatcher] File is locked.");
}
await Task.Delay(150, token);
}
}
public void Dispose()
{
EndConsumerTask();
EndFileWatcher();
}
}

View file

@ -1,39 +0,0 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.EventArgs;
using OtterGui.Text;
using Penumbra.Mods.Manager;
namespace Penumbra.Services;
public class InstallNotification(ModImportManager modImportManager, string filePath) : OtterGui.Classes.MessageService.IMessage
{
public string Message
=> "A new mod has been found!";
public NotificationType NotificationType
=> NotificationType.Info;
public uint NotificationDuration
=> uint.MaxValue;
public string NotificationTitle { get; } = Path.GetFileNameWithoutExtension(filePath);
public string LogMessage
=> $"A new mod has been found: {Path.GetFileName(filePath)}";
public void OnNotificationActions(INotificationDrawArgs args)
{
var region = ImGui.GetContentRegionAvail();
var buttonSize = new Vector2((region.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
if (ImUtf8.ButtonEx("Install"u8, ""u8, buttonSize))
{
modImportManager.AddUnpack(filePath);
args.Notification.DismissNow();
}
ImGui.SameLine();
if (ImUtf8.ButtonEx("Ignore"u8, ""u8, buttonSize))
args.Notification.DismissNow();
}
}

View file

@ -216,7 +216,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable
}
public bool Valid
=> Mtrl.Valid; // FIXME This should be _shadersKnown && Mtrl.Valid but the algorithm for _shadersKnown is flawed as of 7.2.
=> _shadersKnown && Mtrl.Valid;
public byte[] Write()
{

View file

@ -11,7 +11,6 @@ using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;
using OtterGui.Raii;
using OtterGui.Text;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Actors;
@ -223,31 +222,26 @@ public sealed class CollectionPanel(
ImGui.EndGroup();
ImGui.SameLine();
ImGui.BeginGroup();
var width = ImGui.GetContentRegionAvail().X;
using (ImRaii.Disabled(_collections.DefaultNamed == collection))
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
var name = _newName ?? collection.Identity.Name;
var identifier = collection.Identity.Identifier;
var width = ImGui.GetContentRegionAvail().X;
var fileName = saveService.FileNames.CollectionFile(collection);
ImGui.SetNextItemWidth(width);
if (ImGui.InputText("##name", ref name, 128))
_newName = name;
if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
var name = _newName ?? collection.Identity.Name;
ImGui.SetNextItemWidth(width);
if (ImGui.InputText("##name", ref name, 128))
_newName = name;
if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name)
{
collection.Identity.Name = _newName;
saveService.QueueSave(new ModCollectionSave(mods, collection));
selector.RestoreCollections();
_newName = null;
}
else if (ImGui.IsItemDeactivated())
{
_newName = null;
}
collection.Identity.Name = _newName;
saveService.QueueSave(new ModCollectionSave(mods, collection));
selector.RestoreCollections();
_newName = null;
}
else if (ImGui.IsItemDeactivated())
{
_newName = null;
}
if (_collections.DefaultNamed == collection)
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "The Default collection can not be renamed."u8);
var identifier = collection.Identity.Identifier;
var fileName = saveService.FileNames.CollectionFile(collection);
using (ImRaii.PushFont(UiBuilder.MonoFont))
{
if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0)))
@ -381,7 +375,9 @@ public sealed class CollectionPanel(
ImGuiUtil.TextWrapped(type.ToDescription());
switch (type)
{
case CollectionType.Default: ImGui.TextUnformatted("Overruled by any other Assignment."); break;
case CollectionType.Default:
ImGui.TextUnformatted("Overruled by any other Assignment.");
break;
case CollectionType.Yourself:
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0));
break;

View file

@ -116,8 +116,7 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
public void RestoreCollections()
{
Items.Clear();
Items.Add(_storage.DefaultNamed);
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
foreach (var c in _storage.OrderBy(c => c.Identity.Name))
Items.Add(c);
SetFilterDirty();
SetCurrent(_active.Current);

View file

@ -37,7 +37,6 @@ public class SettingsTab : ITab, IUiService
private readonly Penumbra _penumbra;
private readonly FileDialogService _fileDialog;
private readonly ModManager _modManager;
private readonly FileWatcher _fileWatcher;
private readonly ModExportManager _modExportManager;
private readonly ModFileSystemSelector _selector;
private readonly CharacterUtility _characterUtility;
@ -66,8 +65,7 @@ public class SettingsTab : ITab, IUiService
public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial,
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager,
FileWatcher fileWatcher, HttpApi httpApi,
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
@ -84,7 +82,6 @@ public class SettingsTab : ITab, IUiService
_characterUtility = characterUtility;
_residentResources = residentResources;
_modExportManager = modExportManager;
_fileWatcher = fileWatcher;
_httpApi = httpApi;
_dalamudSubstitutionProvider = dalamudSubstitutionProvider;
_compactor = compactor;
@ -650,13 +647,6 @@ public class SettingsTab : ITab, IUiService
DrawDefaultModImportFolder();
DrawPcpFolder();
DrawDefaultModExportPath();
Checkbox("Enable Directory Watcher",
"Enables a File Watcher that automatically listens for Mod files that enter a specified directory, causing Penumbra to open a popup to import these mods.",
_config.EnableDirectoryWatch, _fileWatcher.Toggle);
Checkbox("Enable Fully Automatic Import",
"Uses the File Watcher in order to skip the query popup and automatically import any new mods.",
_config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v);
DrawFileWatcherPath();
}
@ -736,46 +726,6 @@ public class SettingsTab : ITab, IUiService
+ "Keep this empty to use the root directory.");
}
private string? _tempWatchDirectory;
/// <summary> Draw input for the Automatic Mod import path. </summary>
private void DrawFileWatcherPath()
{
var tmp = _tempWatchDirectory ?? _config.WatchDirectory;
var spacing = new Vector2(UiHelpers.ScaleX3);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
if (ImGui.InputText("##fileWatchPath", ref tmp, 256))
_tempWatchDirectory = tmp;
if (ImGui.IsItemDeactivated() && _tempWatchDirectory is not null)
{
if (ImGui.IsItemDeactivatedAfterEdit())
_fileWatcher.UpdateDirectory(_tempWatchDirectory);
_tempWatchDirectory = null;
}
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize,
"Select a directory via dialog.", false, true))
{
var startDir = _config.WatchDirectory.Length > 0 && Directory.Exists(_config.WatchDirectory)
? _config.WatchDirectory
: Directory.Exists(_config.ModDirectory)
? _config.ModDirectory
: null;
_fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) =>
{
if (b)
_fileWatcher.UpdateDirectory(s);
}, startDir, false);
}
style.Pop();
ImGuiUtil.LabeledHelpMarker("Automatic Import Director",
"Choose the Directory the File Watcher listens to.");
}
/// <summary> Draw input for the default name to input as author into newly generated mods. </summary>
private void DrawDefaultModAuthor()
{

View file

@ -5,8 +5,8 @@
"Punchline": "Runtime mod loader and manager.",
"Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra",
"AssemblyVersion": "1.5.1.8",
"TestingAssemblyVersion": "1.5.1.8",
"AssemblyVersion": "1.5.1.0",
"TestingAssemblyVersion": "1.5.1.0",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 13,
@ -18,9 +18,9 @@
"LoadPriority": 69420,
"LoadRequiredState": 2,
"LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.0/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
}
]