mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Compare commits
No commits in common. "master" and "testing_1.5.0.10" have entirely different histories.
master
...
testing_1.
39 changed files with 160 additions and 702 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
|
Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
|
Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit d889f9ef918514a46049725052d378b441915b00
|
Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793
|
Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd
|
||||||
|
|
@ -17,7 +17,7 @@ public class PenumbraApi(
|
||||||
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
||||||
{
|
{
|
||||||
public const int BreakingVersion = 5;
|
public const int BreakingVersion = 5;
|
||||||
public const int FeatureVersion = 13;
|
public const int FeatureVersion = 11;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,11 @@ using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
|
||||||
using Penumbra.Collections.Manager;
|
|
||||||
using Penumbra.GameData.Interop;
|
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
|
|
||||||
namespace Penumbra.Api.Api;
|
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)
|
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
||||||
{
|
{
|
||||||
|
|
@ -31,27 +28,9 @@ public class RedrawApi(RedrawService redrawService, IFramework framework, Collec
|
||||||
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
|
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
|
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
||||||
{
|
{
|
||||||
add => redrawService.GameObjectRedrawn += value;
|
add => redrawService.GameObjectRedrawn += value;
|
||||||
remove => redrawService.GameObjectRedrawn -= value;
|
remove => redrawService.GameObjectRedrawn -= value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using EmbedIO.WebApi;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Mods.Settings;
|
|
||||||
|
|
||||||
namespace Penumbra.Api;
|
namespace Penumbra.Api;
|
||||||
|
|
||||||
|
|
@ -14,15 +13,13 @@ public class HttpApi : IDisposable, IApiService
|
||||||
private partial class Controller : WebApiController
|
private partial class Controller : WebApiController
|
||||||
{
|
{
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
|
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
|
||||||
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
|
|
||||||
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
|
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,12 +65,6 @@ public class HttpApi : IDisposable, IApiService
|
||||||
|
|
||||||
private partial class Controller(IPenumbraApi api, IFramework framework)
|
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()
|
public partial object? GetMods()
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
||||||
|
|
@ -125,7 +116,6 @@ public class HttpApi : IDisposable, IApiService
|
||||||
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
|
Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered.");
|
||||||
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async partial Task FocusMod()
|
public async partial Task FocusMod()
|
||||||
{
|
{
|
||||||
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
|
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);
|
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)
|
private record ModReloadData(string Path, string Name)
|
||||||
{
|
{
|
||||||
public ModReloadData()
|
public ModReloadData()
|
||||||
|
|
@ -185,19 +151,5 @@ public class HttpApi : IDisposable, IApiService
|
||||||
: this(string.Empty, RedrawType.Redraw, -1)
|
: 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)
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ public sealed class IpcProviders : IDisposable, IApiService
|
||||||
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
|
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
|
||||||
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
||||||
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
||||||
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
|
|
||||||
|
|
||||||
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
||||||
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,6 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
ImGui.OpenPopup("Changed Item List");
|
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()
|
private void DrawChangedItemPopup()
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
||||||
|
|
||||||
public string ModDirectory { get; set; } = string.Empty;
|
public string ModDirectory { get; set; } = string.Empty;
|
||||||
public string ExportDirectory { 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? UseCrashHandler { get; set; } = null;
|
||||||
public bool OpenWindowAtStart { get; set; } = false;
|
public bool OpenWindowAtStart { get; set; } = false;
|
||||||
|
|
@ -77,8 +76,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
||||||
public bool HideRedrawBar { get; set; } = false;
|
public bool HideRedrawBar { get; set; } = false;
|
||||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||||
public bool DefaultTemporaryMode { get; set; } = false;
|
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 bool EnableCustomShapes { get; set; } = true;
|
||||||
public PcpSettings PcpSettings = new();
|
public PcpSettings PcpSettings = new();
|
||||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
namespace Penumbra.Interop;
|
|
||||||
|
|
||||||
public static unsafe partial class CloudApi
|
|
||||||
{
|
|
||||||
private const int CfSyncRootInfoBasic = 0;
|
|
||||||
|
|
||||||
/// <summary> Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. </summary>
|
|
||||||
/// <remarks> Can be expensive. Callers should cache the result when relevant. </remarks>
|
|
||||||
public static bool IsCloudSynced(string path)
|
|
||||||
{
|
|
||||||
var buffer = stackalloc long[1];
|
|
||||||
int hr;
|
|
||||||
uint length;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length);
|
|
||||||
}
|
|
||||||
catch (DllNotFoundException)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (EntryPointNotFoundException)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}");
|
|
||||||
if (hr < 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (length != sizeof(long))
|
|
||||||
{
|
|
||||||
Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
[LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)]
|
|
||||||
private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength,
|
|
||||||
out uint returnedLength);
|
|
||||||
}
|
|
||||||
|
|
@ -63,7 +63,8 @@ public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResource
|
||||||
{
|
{
|
||||||
if (timeline != null)
|
if (timeline != null)
|
||||||
{
|
{
|
||||||
var idx = timeline->GetOwningGameObjectIndex();
|
// TODO: Clientstructify
|
||||||
|
var idx = ((delegate* unmanaged<SchedulerTimeline*, int>**)timeline)[0][29](timeline);
|
||||||
if (idx >= 0 && idx < objects.TotalCount)
|
if (idx >= 0 && idx < objects.TotalCount)
|
||||||
{
|
{
|
||||||
var obj = objects[idx];
|
var obj = objects[idx];
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver(
|
||||||
{
|
{
|
||||||
var item = charaEntry.Value;
|
var item = charaEntry.Value;
|
||||||
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
|
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")}.");
|
$"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)
|
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
||||||
_objects.InvokeRequiredUpdates();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,10 @@ public static unsafe class SkinMtrlPathEarlyProcessing
|
||||||
if (character is null)
|
if (character is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (character->PerSlotStagingArea is not null)
|
if (character->TempSlotData is not null)
|
||||||
{
|
{
|
||||||
var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle;
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1564)
|
||||||
|
var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8);
|
||||||
if (handle != null)
|
if (handle != null)
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,10 +242,10 @@ public class ResourceTree(
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, CharacterBase* model, string prefix = "")
|
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, CharacterBase* model, string prefix = "")
|
||||||
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix);
|
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix);
|
||||||
|
|
||||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
|
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
|
||||||
BoneKineDriverModule* kineDriver, string prefix = "")
|
void* kineDriver, string prefix = "")
|
||||||
{
|
{
|
||||||
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
||||||
if (eidNode != null)
|
if (eidNode != null)
|
||||||
|
|
@ -261,7 +261,8 @@ public class ResourceTree(
|
||||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||||
{
|
{
|
||||||
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
|
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
|
||||||
var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null;
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1562)
|
||||||
|
var kdbHandle = kineDriver != null ? *(ResourceHandle**)((nint)kineDriver + 0x20 + 0x18 * i) : null;
|
||||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
|
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
|
||||||
{
|
{
|
||||||
if (context.Global.WithUiData)
|
if (context.Global.WithUiData)
|
||||||
|
|
|
||||||
|
|
@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable
|
||||||
return;
|
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)
|
if (gameObject == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,11 @@ internal static class StructExtensions
|
||||||
|
|
||||||
public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
|
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1561)
|
||||||
|
var vf80 = (delegate* unmanaged<CharacterBase*, byte*, nuint, uint, byte*>)((nint*)character.VirtualTable)[80];
|
||||||
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex));
|
return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize,
|
||||||
|
partialSkeletonIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
|
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,7 @@ public class ModMerger : IDisposable, IService
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// TODO DataContainer <> Option.
|
||||||
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
|
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
|
||||||
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
|
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
|
||||||
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);
|
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Manager.OptionEditor;
|
using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
@ -304,9 +303,6 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
if (!firstTime && _config.ModDirectory != BasePath.FullName)
|
if (!firstTime && _config.ModDirectory != BasePath.FullName)
|
||||||
TriggerModDirectoryChange(BasePath.FullName, Valid);
|
TriggerModDirectoryChange(BasePath.FullName, Valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CloudApi.IsCloudSynced(BasePath.FullName))
|
|
||||||
Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TriggerModDirectoryChange(string newPath, bool valid)
|
private void TriggerModDirectoryChange(string newPath, bool valid)
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ using Penumbra.UI;
|
||||||
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
using Penumbra.GameData;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Interop.Hooks;
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||||
|
|
@ -211,11 +211,10 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
public string GatherSupportInformation()
|
public string GatherSupportInformation()
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(10240);
|
var sb = new StringBuilder(10240);
|
||||||
var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory);
|
var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory);
|
||||||
var cloudSynced = exists && CloudApi.IsCloudSynced(_config.ModDirectory);
|
var hdrEnabler = _services.GetService<RenderTargetHdrEnabler>();
|
||||||
var hdrEnabler = _services.GetService<RenderTargetHdrEnabler>();
|
var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null;
|
||||||
var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null;
|
|
||||||
sb.AppendLine("**Settings**");
|
sb.AppendLine("**Settings**");
|
||||||
sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n");
|
sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n");
|
||||||
sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n");
|
sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n");
|
||||||
|
|
@ -224,8 +223,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n");
|
sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n");
|
||||||
if (Dalamud.Utility.Util.IsWine())
|
if (Dalamud.Utility.Util.IsWine())
|
||||||
sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n");
|
sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n");
|
||||||
sb.Append(
|
sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n");
|
||||||
$"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n");
|
|
||||||
sb.Append(
|
sb.Append(
|
||||||
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
|
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
|
||||||
sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n");
|
sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyTitle>Penumbra</AssemblyTitle>
|
<AssemblyTitle>Penumbra</AssemblyTitle>
|
||||||
<Company>absolute gangstas</Company>
|
<Company>absolute gangstas</Company>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"Author": "Ottermandias, Nylfae, Adam, Wintermute",
|
"Author": "Ottermandias, Adam, Wintermute",
|
||||||
"Name": "Penumbra",
|
"Name": "Penumbra",
|
||||||
"Punchline": "Runtime mod loader and manager.",
|
"Punchline": "Runtime mod loader and manager.",
|
||||||
"Description": "Runtime mod loader and manager.",
|
"Description": "Runtime mod loader and manager.",
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -82,9 +82,9 @@ public class PcpService : IApiService, IDisposable
|
||||||
public void CleanPcpCollections()
|
public void CleanPcpCollections()
|
||||||
{
|
{
|
||||||
var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList();
|
var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList();
|
||||||
Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} collections starting with PCP/.");
|
Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP.");
|
||||||
foreach (var collection in collections)
|
foreach (var collection in collections)
|
||||||
_collections.Storage.RemoveCollection(collection);
|
_collections.Storage.Delete(collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
|
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Valid
|
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()
|
public byte[] Write()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -287,17 +287,6 @@ public partial class ModEditWindow
|
||||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||||
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
|
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
|
||||||
}
|
}
|
||||||
else if (tmp.Length > 0 && Path.GetExtension(tmp) != registry.File.Extension)
|
|
||||||
{
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosX(pos);
|
|
||||||
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
|
||||||
{
|
|
||||||
ImGuiUtil.TextColored(0xFF00B0B0, FontAwesomeIcon.ExclamationCircle.ToIconString());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImUtf8.HoverTooltip("The game path and the file do not have the same extension."u8);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod)
|
private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod)
|
||||||
|
|
@ -330,17 +319,6 @@ public partial class ModEditWindow
|
||||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||||
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
|
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
|
||||||
}
|
}
|
||||||
else if (tmp.Length > 0 && Path.GetExtension(tmp) != registry.File.Extension)
|
|
||||||
{
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosX(pos);
|
|
||||||
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
|
||||||
{
|
|
||||||
ImGuiUtil.TextColored(0xFF00B0B0, FontAwesomeIcon.ExclamationCircle.ToIconString());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImUtf8.HoverTooltip("The game path and the file do not have the same extension."u8);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawButtonHeader()
|
private void DrawButtonHeader()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ public partial class ModEditWindow
|
||||||
private readonly FileDialogService _fileDialog;
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly ResourceTreeFactory _resourceTreeFactory;
|
private readonly ResourceTreeFactory _resourceTreeFactory;
|
||||||
private readonly ResourceTreeViewer _quickImportViewer;
|
private readonly ResourceTreeViewer _quickImportViewer;
|
||||||
|
private readonly Dictionary<FullPath, IWritable?> _quickImportWritables = new();
|
||||||
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
|
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
|
||||||
|
|
||||||
private HashSet<string> GetPlayerResourcesOfType(ResourceType type)
|
private HashSet<string> GetPlayerResourcesOfType(ResourceType type)
|
||||||
|
|
@ -55,11 +56,52 @@ public partial class ModEditWindow
|
||||||
|
|
||||||
private void OnQuickImportRefresh()
|
private void OnQuickImportRefresh()
|
||||||
{
|
{
|
||||||
|
_quickImportWritables.Clear();
|
||||||
_quickImportActions.Clear();
|
_quickImportActions.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawQuickImportActions(ResourceNode resourceNode, IWritable? writable, Vector2 buttonSize)
|
private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize)
|
||||||
{
|
{
|
||||||
|
if (!_quickImportWritables!.TryGetValue(resourceNode.FullPath, out var writable))
|
||||||
|
{
|
||||||
|
var path = resourceNode.FullPath.ToPath();
|
||||||
|
if (resourceNode.FullPath.IsRooted)
|
||||||
|
{
|
||||||
|
writable = new RawFileWritable(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var file = _gameData.GetFile(path);
|
||||||
|
writable = file is null ? null : new RawGameFileWritable(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
_quickImportWritables.Add(resourceNode.FullPath, writable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
|
||||||
|
resourceNode.FullPath.FullName.Length is 0 || writable is null))
|
||||||
|
{
|
||||||
|
var fullPathStr = resourceNode.FullPath.FullName;
|
||||||
|
var ext = resourceNode.PossibleGamePaths.Length == 1
|
||||||
|
? Path.GetExtension(resourceNode.GamePath.ToString())
|
||||||
|
: Path.GetExtension(fullPathStr);
|
||||||
|
_fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext,
|
||||||
|
(success, name) =>
|
||||||
|
{
|
||||||
|
if (!success)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_editor.Compactor.WriteAllBytes(name, writable!.Write());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}");
|
||||||
|
}
|
||||||
|
}, null, false);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport))
|
if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport))
|
||||||
{
|
{
|
||||||
|
|
@ -79,6 +121,24 @@ public partial class ModEditWindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RawFileWritable(string Path) : IWritable
|
||||||
|
{
|
||||||
|
public bool Valid
|
||||||
|
=> true;
|
||||||
|
|
||||||
|
public byte[] Write()
|
||||||
|
=> File.ReadAllBytes(Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RawGameFileWritable(FileResource FileResource) : IWritable
|
||||||
|
{
|
||||||
|
public bool Valid
|
||||||
|
=> true;
|
||||||
|
|
||||||
|
public byte[] Write()
|
||||||
|
=> FileResource.Data;
|
||||||
|
}
|
||||||
|
|
||||||
public class QuickImportAction
|
public class QuickImportAction
|
||||||
{
|
{
|
||||||
public const string FallbackOptionName = "the current option";
|
public const string FallbackOptionName = "the current option";
|
||||||
|
|
|
||||||
|
|
@ -667,7 +667,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
|
||||||
_center = new CombinedTexture(_left, _right);
|
_center = new CombinedTexture(_left, _right);
|
||||||
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
|
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
|
||||||
_resourceTreeFactory = resourceTreeFactory;
|
_resourceTreeFactory = resourceTreeFactory;
|
||||||
_quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions);
|
_quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions);
|
||||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
|
||||||
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
|
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
|
||||||
if (IsOpen && selection.Mod != null)
|
if (IsOpen && selection.Mod != null)
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,16 @@ using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Lumina.Data;
|
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Compression;
|
|
||||||
using OtterGui.Extensions;
|
using OtterGui.Extensions;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData.Files;
|
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
@ -29,20 +25,17 @@ public class ResourceTreeViewer(
|
||||||
IncognitoService incognito,
|
IncognitoService incognito,
|
||||||
int actionCapacity,
|
int actionCapacity,
|
||||||
Action onRefresh,
|
Action onRefresh,
|
||||||
Action<ResourceNode, IWritable?, Vector2> drawActions,
|
Action<ResourceNode, Vector2> drawActions,
|
||||||
CommunicatorService communicator,
|
CommunicatorService communicator,
|
||||||
PcpService pcpService,
|
PcpService pcpService,
|
||||||
IDataManager gameData,
|
IDataManager gameData)
|
||||||
FileDialogService fileDialog,
|
|
||||||
FileCompactor compactor)
|
|
||||||
{
|
{
|
||||||
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
|
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
|
||||||
ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
|
ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
|
||||||
|
|
||||||
private readonly HashSet<nint> _unfolded = [];
|
private readonly HashSet<nint> _unfolded = [];
|
||||||
|
|
||||||
private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
|
private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
|
||||||
private readonly Dictionary<FullPath, IWritable?> _writableCache = [];
|
|
||||||
|
|
||||||
private TreeCategory _categoryFilter = AllCategories;
|
private TreeCategory _categoryFilter = AllCategories;
|
||||||
private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags;
|
private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags;
|
||||||
|
|
@ -122,7 +115,7 @@ public class ResourceTreeViewer(
|
||||||
ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8);
|
ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8);
|
||||||
|
|
||||||
|
|
||||||
using var table = ImRaii.Table("##ResourceTree", 4,
|
using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3,
|
||||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||||
if (!table)
|
if (!table)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -130,8 +123,9 @@ public class ResourceTreeViewer(
|
||||||
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f);
|
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f);
|
||||||
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f);
|
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f);
|
||||||
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
|
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
|
||||||
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed,
|
if (actionCapacity > 0)
|
||||||
actionCapacity * 3 * ImGuiHelpers.GlobalScale + (actionCapacity + 1) * ImGui.GetFrameHeight());
|
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed,
|
||||||
|
(actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight());
|
||||||
ImGui.TableHeadersRow();
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0);
|
DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0);
|
||||||
|
|
@ -217,7 +211,6 @@ public class ResourceTreeViewer(
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_filterCache.Clear();
|
_filterCache.Clear();
|
||||||
_writableCache.Clear();
|
|
||||||
_unfolded.Clear();
|
_unfolded.Clear();
|
||||||
onRefresh();
|
onRefresh();
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +221,7 @@ public class ResourceTreeViewer(
|
||||||
{
|
{
|
||||||
var debugMode = config.DebugMode;
|
var debugMode = config.DebugMode;
|
||||||
var frameHeight = ImGui.GetFrameHeight();
|
var frameHeight = ImGui.GetFrameHeight();
|
||||||
|
var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f;
|
||||||
|
|
||||||
foreach (var (resourceNode, index) in resourceNodes.WithIndex())
|
foreach (var (resourceNode, index) in resourceNodes.WithIndex())
|
||||||
{
|
{
|
||||||
|
|
@ -297,7 +291,7 @@ public class ResourceTreeViewer(
|
||||||
0 => "(none)",
|
0 => "(none)",
|
||||||
1 => resourceNode.GamePath.ToString(),
|
1 => resourceNode.GamePath.ToString(),
|
||||||
_ => "(multiple)",
|
_ => "(multiple)",
|
||||||
}, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
|
}, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
if (hasGamePaths)
|
if (hasGamePaths)
|
||||||
{
|
{
|
||||||
var allPaths = string.Join('\n', resourceNode.PossibleGamePaths);
|
var allPaths = string.Join('\n', resourceNode.PossibleGamePaths);
|
||||||
|
|
@ -318,29 +312,17 @@ public class ResourceTreeViewer(
|
||||||
using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value()))
|
using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value()))
|
||||||
{
|
{
|
||||||
ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap,
|
ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap,
|
||||||
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
|
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosX(textPos);
|
ImGui.SetCursorPosX(textPos);
|
||||||
ImUtf8.Text(resourceNode.ModRelativePath);
|
ImUtf8.Text(resourceNode.ModRelativePath);
|
||||||
}
|
}
|
||||||
else if (resourceNode.FullPath.IsRooted)
|
|
||||||
{
|
|
||||||
var path = resourceNode.FullPath.FullName;
|
|
||||||
var lastDirectorySeparator = path.LastIndexOf('\\');
|
|
||||||
var secondLastDirectorySeparator = lastDirectorySeparator > 0
|
|
||||||
? path.LastIndexOf('\\', lastDirectorySeparator - 1)
|
|
||||||
: -1;
|
|
||||||
if (secondLastDirectorySeparator >= 0)
|
|
||||||
path = $"…{path.AsSpan(secondLastDirectorySeparator)}";
|
|
||||||
ImGui.Selectable(path.AsSpan(), false, ImGuiSelectableFlags.AllowItemOverlap,
|
|
||||||
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap,
|
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap,
|
||||||
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
|
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
|
|
@ -354,17 +336,20 @@ public class ResourceTreeViewer(
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled,
|
ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled,
|
||||||
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
|
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
ImGuiUtil.HoverTooltip(
|
ImGuiUtil.HoverTooltip(
|
||||||
$"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");
|
$"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
mutedColor.Dispose();
|
mutedColor.Dispose();
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
if (actionCapacity > 0)
|
||||||
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
|
{
|
||||||
ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale });
|
ImGui.TableNextColumn();
|
||||||
DrawActions(resourceNode, new Vector2(frameHeight));
|
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
|
||||||
|
ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale });
|
||||||
|
drawActions(resourceNode, new Vector2(frameHeight));
|
||||||
|
}
|
||||||
|
|
||||||
if (unfolded)
|
if (unfolded)
|
||||||
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon);
|
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon);
|
||||||
|
|
@ -417,51 +402,6 @@ public class ResourceTreeViewer(
|
||||||
|| node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
|| node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|
||||||
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
|
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawActions(ResourceNode resourceNode, Vector2 buttonSize)
|
|
||||||
{
|
|
||||||
if (!_writableCache!.TryGetValue(resourceNode.FullPath, out var writable))
|
|
||||||
{
|
|
||||||
var path = resourceNode.FullPath.ToPath();
|
|
||||||
if (resourceNode.FullPath.IsRooted)
|
|
||||||
{
|
|
||||||
writable = new RawFileWritable(path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var file = gameData.GetFile(path);
|
|
||||||
writable = file is null ? null : new RawGameFileWritable(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
_writableCache.Add(resourceNode.FullPath, writable);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
|
|
||||||
resourceNode.FullPath.FullName.Length is 0 || writable is null))
|
|
||||||
{
|
|
||||||
var fullPathStr = resourceNode.FullPath.FullName;
|
|
||||||
var ext = resourceNode.PossibleGamePaths.Length == 1
|
|
||||||
? Path.GetExtension(resourceNode.GamePath.ToString())
|
|
||||||
: Path.GetExtension(fullPathStr);
|
|
||||||
fileDialog.OpenSavePicker($"Export {Path.GetFileName(fullPathStr)} to...", ext, Path.GetFileNameWithoutExtension(fullPathStr), ext,
|
|
||||||
(success, name) =>
|
|
||||||
{
|
|
||||||
if (!success)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
compactor.WriteAllBytes(name, writable!.Write());
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}");
|
|
||||||
}
|
|
||||||
}, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawActions(resourceNode, writable, new Vector2(frameHeight));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReadOnlySpan<byte> GetPathStatusLabel(ResourceNode.PathStatus status)
|
private static ReadOnlySpan<byte> GetPathStatusLabel(ResourceNode.PathStatus status)
|
||||||
|
|
@ -525,22 +465,4 @@ public class ResourceTreeViewer(
|
||||||
Visible = 1,
|
Visible = 1,
|
||||||
DescendentsOnly = 2,
|
DescendentsOnly = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
private record RawFileWritable(string Path) : IWritable
|
|
||||||
{
|
|
||||||
public bool Valid
|
|
||||||
=> true;
|
|
||||||
|
|
||||||
public byte[] Write()
|
|
||||||
=> File.ReadAllBytes(Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private record RawGameFileWritable(FileResource FileResource) : IWritable
|
|
||||||
{
|
|
||||||
public bool Valid
|
|
||||||
=> true;
|
|
||||||
|
|
||||||
public byte[] Write()
|
|
||||||
=> FileResource.Data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using OtterGui.Compression;
|
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Files;
|
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
|
|
@ -14,11 +12,8 @@ public class ResourceTreeViewerFactory(
|
||||||
IncognitoService incognito,
|
IncognitoService incognito,
|
||||||
CommunicatorService communicator,
|
CommunicatorService communicator,
|
||||||
PcpService pcpService,
|
PcpService pcpService,
|
||||||
IDataManager gameData,
|
IDataManager gameData) : IService
|
||||||
FileDialogService fileDialog,
|
|
||||||
FileCompactor compactor) : IService
|
|
||||||
{
|
{
|
||||||
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, IWritable?, Vector2> drawActions)
|
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions)
|
||||||
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData,
|
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData);
|
||||||
fileDialog, compactor);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,27 +63,9 @@ public class PenumbraChangelog : IUiService
|
||||||
Add1_3_6_4(Changelog);
|
Add1_3_6_4(Changelog);
|
||||||
Add1_4_0_0(Changelog);
|
Add1_4_0_0(Changelog);
|
||||||
Add1_5_0_0(Changelog);
|
Add1_5_0_0(Changelog);
|
||||||
Add1_5_1_0(Changelog);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#region Changelogs
|
|
||||||
|
|
||||||
private static void Add1_5_1_0(Changelog log)
|
#region Changelogs
|
||||||
=> log.NextVersion("Version 1.5.1.0")
|
|
||||||
.RegisterHighlight("Added the option to export a characters current data as a .pcp modpack in the On-Screen tab.")
|
|
||||||
.RegisterEntry("Other plugins can attach to this functionality and package and interpret their own data.", 1)
|
|
||||||
.RegisterEntry("When a .pcp modpack is installed, it can create and assign collections for the corresponding character it was created for.", 1)
|
|
||||||
.RegisterEntry("This basically provides an easier way to manually synchronize other players, but does not contain any automation.", 1)
|
|
||||||
.RegisterEntry("The settings provide some fine control about what happens when a PCP is installed, as well as buttons to cleanup any PCP-created data.", 1)
|
|
||||||
.RegisterEntry("Added a warning message when the game's integrity is corrupted to the On-Screen tab.")
|
|
||||||
.RegisterEntry("Added .kdb files to the On-Screen tab and associated functionality (thanks Ny!).")
|
|
||||||
.RegisterEntry("Updated the creation of temporary collections to require a passed identity.")
|
|
||||||
.RegisterEntry("Added the option to change the skin material suffix in models using the stockings shader by adding specific attributes (thanks Ny!).")
|
|
||||||
.RegisterEntry("Added predefined tag utility to the multi-mod selection.")
|
|
||||||
.RegisterEntry("Fixed an issue with the automatic collection selection on character login when no mods are assigned.")
|
|
||||||
.RegisterImportant(
|
|
||||||
"Fixed issue with new deformer data that makes modded deformers not containing this data work implicitly. Updates are still recommended (1.5.0.5).")
|
|
||||||
.RegisterEntry("Fixed various issues after patch (1.5.0.1 - 1.5.0.4).");
|
|
||||||
|
|
||||||
private static void Add1_5_0_0(Changelog log)
|
private static void Add1_5_0_0(Changelog log)
|
||||||
=> log.NextVersion("Version 1.5.0.0")
|
=> log.NextVersion("Version 1.5.0.0")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Extensions;
|
using OtterGui.Extensions;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Text;
|
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
|
|
@ -223,31 +222,26 @@ public sealed class CollectionPanel(
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
var width = ImGui.GetContentRegionAvail().X;
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
|
||||||
using (ImRaii.Disabled(_collections.DefaultNamed == collection))
|
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));
|
collection.Identity.Name = _newName;
|
||||||
var name = _newName ?? collection.Identity.Name;
|
saveService.QueueSave(new ModCollectionSave(mods, collection));
|
||||||
ImGui.SetNextItemWidth(width);
|
selector.RestoreCollections();
|
||||||
if (ImGui.InputText("##name", ref name, 128))
|
_newName = null;
|
||||||
_newName = name;
|
}
|
||||||
if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name)
|
else if (ImGui.IsItemDeactivated())
|
||||||
{
|
{
|
||||||
collection.Identity.Name = _newName;
|
_newName = null;
|
||||||
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))
|
using (ImRaii.PushFont(UiBuilder.MonoFont))
|
||||||
{
|
{
|
||||||
if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0)))
|
if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0)))
|
||||||
|
|
@ -381,7 +375,9 @@ public sealed class CollectionPanel(
|
||||||
ImGuiUtil.TextWrapped(type.ToDescription());
|
ImGuiUtil.TextWrapped(type.ToDescription());
|
||||||
switch (type)
|
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:
|
case CollectionType.Yourself:
|
||||||
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0));
|
ImGuiUtil.DrawColoredText(("Overruled by ", 0), ("Individual ", ColorId.NewMod.Value()), ("Assignments.", 0));
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,7 @@ public sealed class CollectionSelector : ItemSelector<ModCollection>, IDisposabl
|
||||||
public void RestoreCollections()
|
public void RestoreCollections()
|
||||||
{
|
{
|
||||||
Items.Clear();
|
Items.Clear();
|
||||||
Items.Add(_storage.DefaultNamed);
|
foreach (var c in _storage.OrderBy(c => c.Identity.Name))
|
||||||
foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
|
|
||||||
Items.Add(c);
|
Items.Add(c);
|
||||||
SetFilterDirty();
|
SetFilterDirty();
|
||||||
SetCurrent(_active.Current);
|
SetCurrent(_active.Current);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Colors;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
|
|
@ -42,7 +41,6 @@ using Penumbra.GameData.Data;
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||||
using Penumbra.GameData.Files.StainMapStructs;
|
using Penumbra.GameData.Files.StainMapStructs;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.AdvancedWindow.Materials;
|
using Penumbra.UI.AdvancedWindow.Materials;
|
||||||
|
|
||||||
|
|
@ -208,7 +206,6 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
_hookOverrides.Draw();
|
_hookOverrides.Draw();
|
||||||
DrawPlayerModelInfo();
|
DrawPlayerModelInfo();
|
||||||
_globalVariablesDrawer.Draw();
|
_globalVariablesDrawer.Draw();
|
||||||
DrawCloudApi();
|
|
||||||
DrawDebugTabIpc();
|
DrawDebugTabIpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1202,42 +1199,6 @@ public class DebugTab : Window, ITab, IUiService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private string _cloudTesterPath = string.Empty;
|
|
||||||
private bool? _cloudTesterReturn;
|
|
||||||
private Exception? _cloudTesterError;
|
|
||||||
|
|
||||||
private void DrawCloudApi()
|
|
||||||
{
|
|
||||||
if (!ImUtf8.CollapsingHeader("Cloud API"u8))
|
|
||||||
return;
|
|
||||||
|
|
||||||
using var id = ImRaii.PushId("CloudApiTester"u8);
|
|
||||||
|
|
||||||
if (ImUtf8.InputText("Path"u8, ref _cloudTesterPath, flags: ImGuiInputTextFlags.EnterReturnsTrue))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_cloudTesterReturn = CloudApi.IsCloudSynced(_cloudTesterPath);
|
|
||||||
_cloudTesterError = null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_cloudTesterReturn = null;
|
|
||||||
_cloudTesterError = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_cloudTesterReturn.HasValue)
|
|
||||||
ImUtf8.Text($"Is Cloud Synced? {_cloudTesterReturn}");
|
|
||||||
|
|
||||||
if (_cloudTesterError is not null)
|
|
||||||
{
|
|
||||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
|
||||||
ImUtf8.Text($"{_cloudTesterError}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Draw information about IPC options and availability. </summary>
|
/// <summary> Draw information about IPC options and availability. </summary>
|
||||||
private void DrawDebugTabIpc()
|
private void DrawDebugTabIpc()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ using OtterGui.Text;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using Penumbra.Api;
|
using Penumbra.Api;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
|
|
@ -37,7 +36,6 @@ public class SettingsTab : ITab, IUiService
|
||||||
private readonly Penumbra _penumbra;
|
private readonly Penumbra _penumbra;
|
||||||
private readonly FileDialogService _fileDialog;
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly ModManager _modManager;
|
private readonly ModManager _modManager;
|
||||||
private readonly FileWatcher _fileWatcher;
|
|
||||||
private readonly ModExportManager _modExportManager;
|
private readonly ModExportManager _modExportManager;
|
||||||
private readonly ModFileSystemSelector _selector;
|
private readonly ModFileSystemSelector _selector;
|
||||||
private readonly CharacterUtility _characterUtility;
|
private readonly CharacterUtility _characterUtility;
|
||||||
|
|
@ -61,13 +59,9 @@ public class SettingsTab : ITab, IUiService
|
||||||
|
|
||||||
private readonly TagButtons _sharedTags = new();
|
private readonly TagButtons _sharedTags = new();
|
||||||
|
|
||||||
private string _lastCloudSyncTestedPath = string.Empty;
|
|
||||||
private bool _lastCloudSyncTestResult = false;
|
|
||||||
|
|
||||||
public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial,
|
public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial,
|
||||||
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
|
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
|
||||||
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager,
|
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
|
||||||
FileWatcher fileWatcher, HttpApi httpApi,
|
|
||||||
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
|
||||||
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
|
||||||
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
|
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
|
||||||
|
|
@ -84,7 +78,6 @@ public class SettingsTab : ITab, IUiService
|
||||||
_characterUtility = characterUtility;
|
_characterUtility = characterUtility;
|
||||||
_residentResources = residentResources;
|
_residentResources = residentResources;
|
||||||
_modExportManager = modExportManager;
|
_modExportManager = modExportManager;
|
||||||
_fileWatcher = fileWatcher;
|
|
||||||
_httpApi = httpApi;
|
_httpApi = httpApi;
|
||||||
_dalamudSubstitutionProvider = dalamudSubstitutionProvider;
|
_dalamudSubstitutionProvider = dalamudSubstitutionProvider;
|
||||||
_compactor = compactor;
|
_compactor = compactor;
|
||||||
|
|
@ -215,15 +208,6 @@ public class SettingsTab : ITab, IUiService
|
||||||
if (IsSubPathOf(gameDir, newName))
|
if (IsSubPathOf(gameDir, newName))
|
||||||
return ("Path is not allowed to be inside your game folder.", false);
|
return ("Path is not allowed to be inside your game folder.", false);
|
||||||
|
|
||||||
if (_lastCloudSyncTestedPath != newName)
|
|
||||||
{
|
|
||||||
_lastCloudSyncTestResult = CloudApi.IsCloudSynced(newName);
|
|
||||||
_lastCloudSyncTestedPath = newName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_lastCloudSyncTestResult)
|
|
||||||
return ("Path is not allowed to be cloud-synced.", false);
|
|
||||||
|
|
||||||
return selected
|
return selected
|
||||||
? ($"Press Enter or Click Here to Save (Current Directory: {old})", true)
|
? ($"Press Enter or Click Here to Save (Current Directory: {old})", true)
|
||||||
: ($"Click Here to Save (Current Directory: {old})", true);
|
: ($"Click Here to Save (Current Directory: {old})", true);
|
||||||
|
|
@ -650,13 +634,6 @@ public class SettingsTab : ITab, IUiService
|
||||||
DrawDefaultModImportFolder();
|
DrawDefaultModImportFolder();
|
||||||
DrawPcpFolder();
|
DrawPcpFolder();
|
||||||
DrawDefaultModExportPath();
|
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 +713,6 @@ public class SettingsTab : ITab, IUiService
|
||||||
+ "Keep this empty to use the root directory.");
|
+ "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>
|
/// <summary> Draw input for the default name to input as author into newly generated mods. </summary>
|
||||||
private void DrawDefaultModAuthor()
|
private void DrawDefaultModAuthor()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
12
repo.json
12
repo.json
|
|
@ -1,12 +1,12 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Author": "Ottermandias, Nylfae, Adam, Wintermute",
|
"Author": "Ottermandias, Adam, Wintermute",
|
||||||
"Name": "Penumbra",
|
"Name": "Penumbra",
|
||||||
"Punchline": "Runtime mod loader and manager.",
|
"Punchline": "Runtime mod loader and manager.",
|
||||||
"Description": "Runtime mod loader and manager.",
|
"Description": "Runtime mod loader and manager.",
|
||||||
"InternalName": "Penumbra",
|
"InternalName": "Penumbra",
|
||||||
"AssemblyVersion": "1.5.1.8",
|
"AssemblyVersion": "1.5.0.6",
|
||||||
"TestingAssemblyVersion": "1.5.1.8",
|
"TestingAssemblyVersion": "1.5.0.9",
|
||||||
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"DalamudApiLevel": 13,
|
"DalamudApiLevel": 13,
|
||||||
|
|
@ -18,9 +18,9 @@
|
||||||
"LoadPriority": 69420,
|
"LoadPriority": 69420,
|
||||||
"LoadRequiredState": 2,
|
"LoadRequiredState": 2,
|
||||||
"LoadSync": true,
|
"LoadSync": true,
|
||||||
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
|
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip",
|
||||||
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
|
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.9/Penumbra.zip",
|
||||||
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip",
|
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip",
|
||||||
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
|
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue