From 9fc572ba0cb198a3acbf0bd98a696e243c2bc433 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 15 Jun 2025 21:47:41 +0000 Subject: [PATCH 001/108] [CI] Updating repo.json for testing_1.4.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a6cec268..e12c3c9d 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.3", + "TestingAssemblyVersion": "1.4.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 62e9dc164d624c13ef13d023e4501ee1e4f755eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 Jun 2025 14:49:12 +0200 Subject: [PATCH 002/108] Add support button. --- OtterGui | 2 +- Penumbra/Penumbra.cs | 6 ++++-- Penumbra/UI/Tabs/SettingsTab.cs | 11 +++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/OtterGui b/OtterGui index 78528f93..2c3c32bf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 78528f93ac253db0061d9a8244cfa0cee5c2f873 +Subproject commit 2c3c32bfb7057d7be7678f413122c2b1453050d5 diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 70636bbf..cf96c7f6 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -229,10 +229,12 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\n"); - sb.Append($"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); + sb.Append( + $"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n"); sb.Append($"> **`Custom Shapes Enabled: `** {_config.EnableCustomShapes}\n"); sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); - sb.Append($"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); + sb.Append( + $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n"); sb.Append( $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4bbdf2a9..c1aea97c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; +using OtterGuiInternal.Enums; using Penumbra.Api; using Penumbra.Collections; using Penumbra.Interop.Hooks.PostProcessing; @@ -20,6 +21,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; +using ImGuiId = OtterGuiInternal.Enums.ImGuiId; namespace Penumbra.UI.Tabs; @@ -113,6 +115,7 @@ public class SettingsTab : ITab, IUiService DrawRootFolder(); DrawDirectoryButtons(); ImGui.NewLine(); + ImGui.NewLine(); DrawGeneralSettings(); _migrationDrawer.Draw(); @@ -761,8 +764,9 @@ public class SettingsTab : ITab, IUiService "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); - Checkbox("Enable Custom Shape and Attribute Support", "Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined.", - _config.EnableCustomShapes, _attributeHook.SetState); + Checkbox("Enable Custom Shape and Attribute Support", + "Penumbra will allow for custom shape keys and attributes for modded models to be considered and combined.", + _config.EnableCustomShapes, _attributeHook.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); @@ -1050,6 +1054,9 @@ public class SettingsTab : ITab, IUiService ImGui.SetCursorPos(new Vector2(xPos, 4 * ImGui.GetFrameHeightWithSpacing())); if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); + + ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing())); + CustomGui.DrawKofiPatreonButton(Penumbra.Messager, new Vector2(width, 0)); } private void DrawPredefinedTagsSection() From 30e3cd1f383ce9c9c2a1e2927518993d9be9ad1c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 4 Jul 2025 19:41:31 +0200 Subject: [PATCH 003/108] Add OS thread ID info to the Resource Logger --- Penumbra/Interop/ProcessThreadApi.cs | 7 +++++++ Penumbra/UI/ResourceWatcher/Record.cs | 9 +++++++++ .../UI/ResourceWatcher/ResourceWatcherTable.cs | 18 +++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Interop/ProcessThreadApi.cs diff --git a/Penumbra/Interop/ProcessThreadApi.cs b/Penumbra/Interop/ProcessThreadApi.cs new file mode 100644 index 00000000..5ee213d9 --- /dev/null +++ b/Penumbra/Interop/ProcessThreadApi.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Interop; + +public static partial class ProcessThreadApi +{ + [LibraryImport("kernel32.dll")] + public static partial uint GetCurrentThreadId(); +} diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index b8730750..ba718bc9 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Enums; +using Penumbra.Interop; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; @@ -34,6 +35,7 @@ internal unsafe struct Record public OptionalBool ReturnValue; public OptionalBool CustomLoad; public LoadState LoadState; + public uint OsThreadId; public static Record CreateRequest(CiByteString path, bool sync) @@ -54,6 +56,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = LoadState.None, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateRequest(CiByteString path, bool sync, FullPath fullPath, ResolveData resolve) @@ -74,6 +77,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = LoadState.None, Crc64 = fullPath.Crc64, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) @@ -96,6 +100,7 @@ internal unsafe struct Record AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; } @@ -118,6 +123,7 @@ internal unsafe struct Record AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, Crc64 = path.Crc64, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateDestruction(ResourceHandle* handle) @@ -140,6 +146,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; } @@ -161,6 +168,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan additionalData) @@ -181,6 +189,7 @@ internal unsafe struct Record AssociatedGameObject = string.Empty, LoadState = handle->LoadState, Crc64 = 0, + OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan additionalData) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index a58d74d1..009da842 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -30,7 +30,8 @@ internal sealed class ResourceWatcherTable : Table new LoadStateColumn { Label = "State" }, new RefCountColumn { Label = "#Ref" }, new DateColumn { Label = "Time" }, - new Crc64Column { Label = "Crc64" } + new Crc64Column { Label = "Crc64" }, + new OsThreadColumn { Label = "TID" } ) { } @@ -453,4 +454,19 @@ internal sealed class ResourceWatcherTable : Table public override int Compare(Record lhs, Record rhs) => lhs.RefCount.CompareTo(rhs.RefCount); } + + private sealed class OsThreadColumn : ColumnString + { + public override float Width + => 60 * UiHelpers.Scale; + + public override string ToName(Record item) + => item.OsThreadId.ToString(); + + public override void DrawColumn(Record item, int _) + => ImGuiUtil.RightAlign(ToName(item)); + + public override int Compare(Record lhs, Record rhs) + => lhs.OsThreadId.CompareTo(rhs.OsThreadId); + } } From a97d9e49531ace985eeef1c305bdd123b11dfa38 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 5 Jul 2025 04:37:37 +0200 Subject: [PATCH 004/108] Add Human skin material handling --- .../Hooks/Resources/ResolvePathHooksBase.cs | 40 ++++++++----- .../Interop/ResourceTree/ResolveContext.cs | 9 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 12 +++- Penumbra/Interop/Structs/StructExtensions.cs | 60 +++++++++++-------- 4 files changed, 76 insertions(+), 45 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 8a45ec2c..85fb1098 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -35,6 +35,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private readonly Hook _resolveMPapPathHook; private readonly Hook _resolveMdlPathHook; private readonly Hook _resolveMtrlPathHook; + private readonly Hook _resolveSkinMtrlPathHook; private readonly Hook _resolvePapPathHook; private readonly Hook _resolveKdbPathHook; private readonly Hook _resolvePhybPathHook; @@ -52,22 +53,23 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable { _parent = parent; // @formatter:off - _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman); - _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); - _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); - _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); - _resolveKdbPathHook = Create($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman); - _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); - _resolveBnmbPathHook = Create($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman); - _vFunc83Hook = Create( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); - _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); - _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); - _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); - _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc); - _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl); - _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); - _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); - _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); + _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman); + _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman); + _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); + _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); + _resolveKdbPathHook = Create($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman); + _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); + _resolveBnmbPathHook = Create($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman); + _vFunc83Hook = Create( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83); + _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman); + _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb); + _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap); + _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc); + _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl); + _resolveSkinMtrlPathHook = Create($"{name}.{nameof(ResolveSkinMtrl)}", hooks, vTable[91], ResolveSkinMtrl); + _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal); + _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); + _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); // @formatter:on @@ -83,6 +85,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMPapPathHook.Enable(); _resolveMdlPathHook.Enable(); _resolveMtrlPathHook.Enable(); + _resolveSkinMtrlPathHook.Enable(); _resolvePapPathHook.Enable(); _resolveKdbPathHook.Enable(); _resolvePhybPathHook.Enable(); @@ -103,6 +106,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMPapPathHook.Disable(); _resolveMdlPathHook.Disable(); _resolveMtrlPathHook.Disable(); + _resolveSkinMtrlPathHook.Disable(); _resolvePapPathHook.Disable(); _resolveKdbPathHook.Disable(); _resolvePhybPathHook.Disable(); @@ -123,6 +127,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMPapPathHook.Dispose(); _resolveMdlPathHook.Dispose(); _resolveMtrlPathHook.Dispose(); + _resolveSkinMtrlPathHook.Dispose(); _resolvePapPathHook.Dispose(); _resolveKdbPathHook.Dispose(); _resolvePhybPathHook.Dispose(); @@ -153,6 +158,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName) => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); + private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) + => ResolvePath(drawObject, _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 013d7db7..64a91302 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -188,7 +188,8 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); } - public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) + public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, + MaterialResourceHandle* skinMtrlHandle, ResourceHandle* mpapHandle) { if (mdl is null || mdl->ModelResourceHandle is null) return null; @@ -218,6 +219,12 @@ internal unsafe partial record ResolveContext( } } + if (skinMtrlHandle is not null + && Utf8GamePath.FromByteString(CharacterBase->ResolveSkinMtrlPathAsByteString(SlotIndex), out var skinMtrlPath) + && CreateNodeFromMaterial(skinMtrlHandle->Material, skinMtrlPath) is + { } skinMaaterialNode) + node.Children.Add(skinMaaterialNode); + if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode) node.Children.Add(decalNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 7be8694a..97a926ad 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -73,6 +73,12 @@ public class ResourceTree( // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) + var skinMtrlArray = modelType switch + { + ModelType.Human => new ReadOnlySpan>((MaterialResourceHandle**)((nint)model + 0xB48), 5), + _ => [], + }; var decalArray = modelType switch { ModelType.Human => human->SlotDecalsSpan, @@ -108,7 +114,8 @@ public class ResourceTree( var mdl = model->Models[i]; if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, - i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode) + i < skinMtrlArray.Length ? skinMtrlArray[(int)i].Value : null, i < mpapArray.Length ? mpapArray[(int)i].Value : null) is + { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Model #{i}"; @@ -166,7 +173,8 @@ public class ResourceTree( } var mdl = subObject->Models[i]; - if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode) + if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, null, i < mpapArray.Length ? mpapArray[i].Value : null) is + { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 03b4cf36..62dca02e 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -10,28 +10,36 @@ internal static class StructExtensions public static CiByteString AsByteString(in this StdString str) => CiByteString.FromSpanUnsafe(str.AsSpan(), true); - public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); - } - - public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); + public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); } - public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); + public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); } - public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) - { - var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); + public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); + } + + public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) + { + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); + } + + public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex) + { + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) + var vf91 = (delegate* unmanaged)((nint*)character.VirtualTable)[91]; + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(vf91((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, slotIndex)); } public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) @@ -40,16 +48,16 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId)); } - public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); + public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); } - public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) - { - Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); + public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); } public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) From 278bf43b29809ff4c0657921311f8581c820b9b8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 5 Jul 2025 05:20:24 +0200 Subject: [PATCH 005/108] ClientStructs-ify ResourceTree stuff --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 97a926ad..e7c4b11b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -70,8 +70,7 @@ public class ResourceTree( var genericContext = globalContext.CreateContext(model); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); + var mpapArrayPtr = model->MaterialAnimationPacks; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) var skinMtrlArray = modelType switch @@ -124,8 +123,7 @@ public class ResourceTree( } AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940)); + AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton); AddWeapons(globalContext, model); @@ -156,8 +154,7 @@ public class ResourceTree( var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948); + var mpapArrayPtr = subObject->MaterialAnimationPacks; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; for (var i = 0; i < subObject->SlotCount; ++i) @@ -184,8 +181,7 @@ public class ResourceTree( AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, "); - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), + AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton, $"Weapon #{weaponIndex}, "); ++weaponIndex; @@ -263,7 +259,7 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1475) var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) { From a953febfba522579b338d2c8015e3dcfd6315168 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Jul 2025 21:59:20 +0200 Subject: [PATCH 006/108] Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. --- Penumbra/Meta/ShapeAttributeManager.cs | 64 +++++++++++++++++++++++++ Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 49 ++++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index a742806f..16901741 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -5,6 +6,8 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; @@ -58,11 +61,72 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable _ids[(int)_modelIndex] = model.GetModelId(_modelIndex); CheckShapes(collection.MetaCache!.Shp); CheckAttributes(collection.MetaCache!.Atr); + if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears) + AccessoryImcCheck(model); } UpdateDefaultMasks(model, collection.MetaCache!.Shp); } + private void AccessoryImcCheck(Model model) + { + var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex)); + + Span attr = + [ + (byte)'a', + (byte)'t', + (byte)'r', + (byte)'_', + AccessoryByte(_modelIndex), + (byte)'v', + (byte)'_', + (byte)'a', + 0, + ]; + for (var i = 1; i < 10; ++i) + { + var flag = (ushort)(1 << i); + if ((imcMask & flag) is not 0) + continue; + + attr[^2] = (byte)('a' + i); + + foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes) + { + if (!EqualAttribute(attr, attribute.Value)) + continue; + + _model->EnabledAttributeIndexMask &= ~(1u << index); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + private static bool EqualAttribute(Span needle, byte* haystack) + { + foreach (var character in needle) + { + if (*haystack++ != character) + return false; + } + + return true; + } + + private static byte AccessoryByte(HumanSlot slot) + => slot switch + { + HumanSlot.Head => (byte)'m', + HumanSlot.Ears => (byte)'e', + HumanSlot.Neck => (byte)'n', + HumanSlot.Wrists => (byte)'w', + HumanSlot.RFinger => (byte)'r', + HumanSlot.LFinger => (byte)'r', + _ => 0, + }; + private void CheckAttributes(AtrCache attributeCache) { if (attributeCache.DisabledCount is 0) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 5c67df52..216b5841 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -234,9 +234,56 @@ public static class EquipmentSwap mdl.ChildSwaps.Add(mtrl); } + FixAttributes(mdl, slotFrom, slotTo); + return mdl; } + private static void FixAttributes(FileSwap swap, EquipSlot slotFrom, EquipSlot slotTo) + { + if (slotFrom == slotTo) + return; + + var needle = slotTo switch + { + EquipSlot.Head => "atr_mv_", + EquipSlot.Ears => "atr_ev_", + EquipSlot.Neck => "atr_nv_", + EquipSlot.Wrists => "atr_wv_", + EquipSlot.RFinger or EquipSlot.LFinger => "atr_rv_", + _ => string.Empty, + }; + + var replacement = slotFrom switch + { + EquipSlot.Head => 'm', + EquipSlot.Ears => 'e', + EquipSlot.Neck => 'n', + EquipSlot.Wrists => 'w', + EquipSlot.RFinger or EquipSlot.LFinger => 'r', + _ => 'm', + }; + + var attributes = swap.AsMdl()!.Attributes; + for (var i = 0; i < attributes.Length; ++i) + { + if (FixAttribute(ref attributes[i], needle, replacement)) + swap.DataWasChanged = true; + } + } + + private static unsafe bool FixAttribute(ref string attribute, string from, char to) + { + if (!attribute.StartsWith(from) || attribute.Length != from.Length + 1 || attribute[^1] is < 'a' or > 'j') + return false; + + Span stack = stackalloc char[attribute.Length]; + attribute.CopyTo(stack); + stack[4] = to; + attribute = new string(stack); + return true; + } + private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant) { slot = i.Type.ToSlot(); @@ -399,7 +446,7 @@ public static class EquipmentSwap return null; var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo); - var pathTo = $"{folderTo}{fileName}"; + var pathTo = $"{folderTo}{fileName}"; var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo); var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom); From 49a6d935f3d0bd149442003f727f0f8a85b07019 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 5 Jul 2025 20:11:28 +0000 Subject: [PATCH 007/108] [CI] Updating repo.json for testing_1.4.0.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e12c3c9d..f0bf9a4a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.4", + "TestingAssemblyVersion": "1.4.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 140d150bb4b1c64051673811aa8cf349b2c56c80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Jul 2025 17:08:46 +0200 Subject: [PATCH 008/108] Fix character sound data. --- Penumbra/Interop/GameState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 95cef468..32b45b7e 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -60,7 +60,7 @@ public class GameState : IService private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); public ResolveData SoundData - => _animationLoadData.Value; + => _characterSoundData.Value; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public ResolveData SetSoundData(ResolveData data) From 00c02fd16e641eb40933010c48ea1e32602213b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 14 Jul 2025 17:09:07 +0200 Subject: [PATCH 009/108] Fix tex file migration for small textures. --- Penumbra/Import/Textures/TexFileParser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 6a12a0dd..04bbf5d8 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -95,7 +95,8 @@ public static class TexFileParser if (width == minSize && height == minSize) { - newSize = totalSize; + ++i; + newSize = totalSize + requiredSize; if (header.MipCount != i) { Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints."); From a4a6283e7b007a5d4f019ee12565887eabc235ad Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 14 Jul 2025 15:12:06 +0000 Subject: [PATCH 010/108] [CI] Updating repo.json for testing_1.4.0.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f0bf9a4a..af368d75 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.5", + "TestingAssemblyVersion": "1.4.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a9546e31eed28555f00bd724f9c00106a40e9da3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:05:27 +0200 Subject: [PATCH 011/108] Update packages. --- .../Import/Models/Import/VertexAttribute.cs | 2 +- Penumbra/Penumbra.csproj | 10 ++--- Penumbra/packages.lock.json | 44 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index a1c3246b..155fa833 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -319,7 +319,7 @@ public class VertexAttribute var normals = normalAccessor.AsVector3Array(); var tangents = accessors.TryGetValue("TANGENT", out var accessor) - ? accessor.AsVector4Array() + ? accessor.AsVector4Array().ToArray() : CalculateTangents(accessors, indices, normals, notifier); if (tangents == null) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f668f775..c61692f4 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -57,11 +57,11 @@ - - - - - + + + + + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 4a162f8f..778f776e 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -19,9 +19,9 @@ }, "PeNet": { "type": "Direct", - "requested": "[4.1.1, )", - "resolved": "4.1.1", - "contentHash": "TiRyOVcg1Bh2FyP6Dm2NEiYzemSlQderhaxuH3XWNyTYsnHrm1n/xvoTftgMwsWD4C/3kTqJw93oZOvHojJfKg==", + "requested": "[5.1.0, )", + "resolved": "5.1.0", + "contentHash": "XSd1PUwWo5uI8iqVHk7Mm02RT1bjndtAYsaRwLmdYZoHOAmb4ohkvRcZiqxJ7iLfBfdiwm+PHKQIMqDmOavBtw==", "dependencies": { "PeNet.Asn1": "2.0.1", "System.Security.Cryptography.Pkcs": "8.0.1" @@ -29,34 +29,34 @@ }, "SharpCompress": { "type": "Direct", - "requested": "[0.39.0, )", - "resolved": "0.39.0", - "contentHash": "0esqIUDlg68Z7+Weuge4QzEvNtawUO4obTJFL7xuf4DBHMxVRr+wbNgiX9arMrj3kGXQSvLe0zbZG3oxpkwJOA==", + "requested": "[0.40.0, )", + "resolved": "0.40.0", + "contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==", "dependencies": { "System.Buffers": "4.6.0", - "ZstdSharp.Port": "0.8.4" + "ZstdSharp.Port": "0.8.5" } }, "SharpGLTF.Core": { "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "su+Flcg2g6GgOIgulRGBDMHA6zY5NBx6NYH1Ayd6iBbSbwspHsN2VQgZfANgJy92cBf7qtpjC0uMiShbO+TEEg==" + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "HNHKPqaHXm7R1nlXZ764K5UI02IeDOQ5DQKLjwYUVNTsSW27jJpw+wLGQx6ZFoiFYqUlyZjmsu+WfEak2GmJAg==" }, "SharpGLTF.Toolkit": { "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vkEuf8ch76NNgZXU/3zoXTIXRO0o14H3aRoSFzcuUQb0PTxvV6jEfmWkUVO6JtLDuFCIimqZaf3hdxr32ltpfQ==", + "requested": "[1.0.5, )", + "resolved": "1.0.5", + "contentHash": "piQKk7PH2pSWQSQmCSd8cYPaDtAy/ppAD+Mrh2RUhhHI8awl81HqqLyAauwQhJwea3LNaiJ6f4ehZuOGk89TlA==", "dependencies": { - "SharpGLTF.Runtime": "1.0.3" + "SharpGLTF.Runtime": "1.0.5" } }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.7, )", - "resolved": "3.1.7", - "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" + "requested": "[3.1.11, )", + "resolved": "3.1.11", + "contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw==" }, "JetBrains.Annotations": { "type": "Transitive", @@ -83,10 +83,10 @@ }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "W0bg2WyXlcSAJVu153hNUNm+BU4RP46yLwGD4099hSm8dsXG/H+J95PBoLJbIq8KGVkUWvfM0+XWHoEkCyd50A==", + "resolved": "1.0.5", + "contentHash": "EVP32k4LqERxSVICiupT8xQvhHSHJCiXajBjNpqdfdajtREHayuVhH0Jmk6uSjTLX8/IIH9XfT34sw3TwvCziw==", "dependencies": { - "SharpGLTF.Core": "1.0.3" + "SharpGLTF.Core": "1.0.5" } }, "System.Buffers": { @@ -114,8 +114,8 @@ }, "ZstdSharp.Port": { "type": "Transitive", - "resolved": "0.8.4", - "contentHash": "eieSXq3kakCUXbgdxkKaRqWS6hF0KBJcqok9LlDCs60GOyrynLvPOcQ0pRw7shdPF7lh/VepJ9cP9n9HHc759g==" + "resolved": "0.8.5", + "contentHash": "TR4j17WeVSEb3ncgL2NqlXEqcy04I+Kk9CaebNDplUeL8XOgjkZ7fP4Wg4grBdPLIqsV86p2QaXTkZoRMVOsew==" }, "ottergui": { "type": "Project", From 012052daa0c5cf81e11c32bb27ee220533000577 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:06:03 +0200 Subject: [PATCH 012/108] Change behavior for directory names. --- Penumbra/Mods/Editor/ModNormalizer.cs | 4 ++-- .../Mods/SubMods/CombinedDataContainer.cs | 19 +++++++++++++++++++ Penumbra/Mods/SubMods/DefaultSubMod.cs | 3 +++ Penumbra/Mods/SubMods/IModDataContainer.cs | 1 + Penumbra/Mods/SubMods/OptionSubMod.cs | 3 +++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 527dbf7c..df1528f6 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -76,7 +76,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ else { var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport); - var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport); + var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetDirectoryName(), config.ReplaceNonAsciiOnImport); containers[container] = optionDir.FullName; } } @@ -286,7 +286,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { - var name = option.GetName(); + var name = option.GetDirectoryName(); var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true); newDict.Clear(); diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs index b467c360..bfca2afd 100644 --- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -48,6 +48,25 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer return sb.ToString(0, sb.Length - 3); } + public unsafe string GetDirectoryName() + { + if (Name.Length > 0) + return Name; + + var index = GetDataIndex(); + if (index == 0) + return "None"; + + var text = stackalloc char[IModGroup.MaxCombiningOptions].Slice(0, Group.Options.Count); + for (var i = 0; i < Group.Options.Count; ++i) + { + text[Group.Options.Count - 1 - i] = (index & 1) is 0 ? '0' : '1'; + index >>= 1; + } + + return new string(text); + } + public string GetFullName() => $"{Group.Name}: {GetName()}"; diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 3840468f..3282f518 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -27,6 +27,9 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public string GetName() => FullName; + public string GetDirectoryName() + => GetName(); + public string GetFullName() => FullName; diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 1a89ec17..92ccf7e1 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -15,6 +15,7 @@ public interface IModDataContainer public MetaDictionary Manipulations { get; set; } public string GetName(); + public string GetDirectoryName(); public string GetFullName(); public (int GroupIndex, int DataIndex) GetDataIndices(); } diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 9044350d..aa3fed8f 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -41,6 +41,9 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public string GetName() => Name; + public string GetDirectoryName() + => GetName(); + public string GetFullName() => FullName; From dc93eba34c322b20dcbbc0164ad2f2379f0909e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:06:25 +0200 Subject: [PATCH 013/108] Add initial complex group things. --- Penumbra/Mods/Groups/ComplexModGroup.cs | 180 ++++++++++++++++++ Penumbra/Mods/Groups/IModGroup.cs | 2 + Penumbra/Mods/SubMods/ComplexDataContainer.cs | 46 +++++ Penumbra/Mods/SubMods/ComplexSubMod.cs | 39 ++++ Penumbra/Mods/SubMods/MaskedSetting.cs | 27 +++ 5 files changed, 294 insertions(+) create mode 100644 Penumbra/Mods/Groups/ComplexModGroup.cs create mode 100644 Penumbra/Mods/SubMods/ComplexDataContainer.cs create mode 100644 Penumbra/Mods/SubMods/ComplexSubMod.cs create mode 100644 Penumbra/Mods/SubMods/MaskedSetting.cs diff --git a/Penumbra/Mods/Groups/ComplexModGroup.cs b/Penumbra/Mods/Groups/ComplexModGroup.cs new file mode 100644 index 00000000..435bc253 --- /dev/null +++ b/Penumbra/Mods/Groups/ComplexModGroup.cs @@ -0,0 +1,180 @@ +using Dalamud.Interface.ImGuiNotification; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using OtterGui.Extensions; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; +using Penumbra.Util; + +namespace Penumbra.Mods.Groups; + +public sealed class ComplexModGroup(Mod mod) : IModGroup +{ + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; + + public GroupType Type + => GroupType.Complex; + + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.Complex; + + public ModPriority Priority { get; set; } + public int Page { get; set; } + public Setting DefaultSettings { get; set; } + + public readonly List Options = []; + public readonly List Containers = []; + + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => throw new NotImplementedException(); + + public IModOption? AddOption(string name, string description = "") + => throw new NotImplementedException(); + + IReadOnlyList IModGroup.Options + => Options; + + IReadOnlyList IModGroup.DataContainers + => Containers; + + public bool IsOption + => Options.Count > 0; + + public int GetIndex() + => ModGroup.GetIndex(this); + + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => throw new NotImplementedException(); + + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) + { + foreach (var container in Containers.Where(c => c.Association.IsEnabled(setting))) + SubMod.AddContainerTo(container, redirections, manipulations); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in Containers) + identifier.AddChangedItems(container, changedItems); + } + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << Options.Count) - 1)); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in Options) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + if (!option.Conditions.IsZero) + { + jWriter.WritePropertyName("ConditionMask"); + jWriter.WriteValue(option.Conditions.Mask.Value); + jWriter.WritePropertyName("ConditionValue"); + jWriter.WriteValue(option.Conditions.Value.Value); + } + + if (option.Indentation > 0) + { + jWriter.WritePropertyName("Indentation"); + jWriter.WriteValue(option.Indentation); + } + + if (option.SubGroupLabel.Length > 0) + { + jWriter.WritePropertyName("SubGroup"); + jWriter.WriteValue(option.SubGroupLabel); + } + + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + + jWriter.WritePropertyName("Containers"); + jWriter.WriteStartArray(); + foreach (var container in Containers) + { + jWriter.WriteStartObject(); + if (container.Name.Length > 0) + { + jWriter.WritePropertyName("Name"); + jWriter.WriteValue(container.Name); + } + + if (!container.Association.IsZero) + { + jWriter.WritePropertyName("AssociationMask"); + jWriter.WriteValue(container.Association.Mask.Value); + + jWriter.WritePropertyName("AssociationValue"); + jWriter.WriteValue(container.Association.Value.Value); + } + + SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => ModGroup.GetCountsBase(this); + + public static ComplexModGroup? Load(Mod mod, JObject json) + { + var ret = new ComplexModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) + return null; + + var options = json["Options"]; + if (options != null) + foreach (var child in options.Children()) + { + if (ret.Options.Count == IModGroup.MaxComplexOptions) + { + Penumbra.Messager.NotificationMessage( + $"Complex Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxComplexOptions} options, ignoring excessive options.", + NotificationType.Warning); + break; + } + + var subMod = new ComplexSubMod(ret, child); + ret.Options.Add(subMod); + } + + // Fix up conditions: No condition on itself. + foreach (var (option, index) in ret.Options.WithIndex()) + { + option.Conditions = option.Conditions.Limit(ret.Options.Count); + option.Conditions = new MaskedSetting(option.Conditions.Mask.SetBit(index, false), option.Conditions.Value); + } + + var containers = json["Containers"]; + if (containers != null) + foreach (var child in containers.Children()) + { + var container = new ComplexDataContainer(ret, child); + container.Association = container.Association.Limit(ret.Options.Count); + ret.Containers.Add(container); + } + + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + + return ret; + } +} diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index cc961b0f..98f62862 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -18,11 +18,13 @@ public enum GroupDrawBehaviour { SingleSelection, MultiSelection, + Complex, } public interface IModGroup { public const int MaxMultiOptions = 32; + public const int MaxComplexOptions = MaxMultiOptions; public const int MaxCombiningOptions = 8; public Mod Mod { get; } diff --git a/Penumbra/Mods/SubMods/ComplexDataContainer.cs b/Penumbra/Mods/SubMods/ComplexDataContainer.cs new file mode 100644 index 00000000..0f0fdef8 --- /dev/null +++ b/Penumbra/Mods/SubMods/ComplexDataContainer.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Extensions; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public sealed class ComplexDataContainer(ComplexModGroup group) : IModDataContainer +{ + public IMod Mod + => Group.Mod; + + public IModGroup Group { get; } = group; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); + + public MaskedSetting Association = MaskedSetting.Zero; + + public string Name { get; set; } = string.Empty; + + public string GetName() + => Name.Length > 0 ? Name : $"Container {Group.DataContainers.IndexOf(this)}"; + + public string GetDirectoryName() + => Name.Length > 0 ? Name : $"{Group.DataContainers.IndexOf(this)}"; + + public string GetFullName() + => $"{Group.Name}: {GetName()}"; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), Group.DataContainers.IndexOf(this)); + + public ComplexDataContainer(ComplexModGroup group, JToken json) + : this(group) + { + SubMod.LoadDataContainer(json, this, group.Mod.ModPath); + var mask = json["AssociationMask"]?.ToObject() ?? 0; + var value = json["AssociationMask"]?.ToObject() ?? 0; + Association = new MaskedSetting(mask, value); + Name = json["Name"]?.ToObject() ?? string.Empty; + } +} diff --git a/Penumbra/Mods/SubMods/ComplexSubMod.cs b/Penumbra/Mods/SubMods/ComplexSubMod.cs new file mode 100644 index 00000000..3eea6f15 --- /dev/null +++ b/Penumbra/Mods/SubMods/ComplexSubMod.cs @@ -0,0 +1,39 @@ +using ImSharp; +using Newtonsoft.Json.Linq; +using OtterGui.Extensions; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; + +namespace Penumbra.Mods.SubMods; + +public sealed class ComplexSubMod(ComplexModGroup group) : IModOption +{ + public Mod Mod + => group.Mod; + + public IModGroup Group { get; } = group; + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public MaskedSetting Conditions = MaskedSetting.Zero; + public int Indentation = 0; + public string SubGroupLabel = string.Empty; + + public string Description { get; set; } = string.Empty; + + public int GetIndex() + => Group.Options.IndexOf(this); + + public ComplexSubMod(ComplexModGroup group, JToken json) + : this(group) + { + SubMod.LoadOptionData(json, this); + var mask = json["ConditionMask"]?.ToObject() ?? 0; + var value = json["ConditionMask"]?.ToObject() ?? 0; + Conditions = new MaskedSetting(mask, value); + Indentation = json["Indentation"]?.ToObject() ?? 0; + SubGroupLabel = json["SubGroup"]?.ToObject() ?? string.Empty; + } +} diff --git a/Penumbra/Mods/SubMods/MaskedSetting.cs b/Penumbra/Mods/SubMods/MaskedSetting.cs new file mode 100644 index 00000000..75bb46c2 --- /dev/null +++ b/Penumbra/Mods/SubMods/MaskedSetting.cs @@ -0,0 +1,27 @@ +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; + +namespace Penumbra.Mods.SubMods; + +public readonly struct MaskedSetting(Setting mask, Setting value) +{ + public const int MaxSettings = IModGroup.MaxMultiOptions; + public static readonly MaskedSetting Zero = new(Setting.Zero, Setting.Zero); + public static readonly MaskedSetting FullMask = new(Setting.AllBits(IModGroup.MaxComplexOptions), Setting.Zero); + + public readonly Setting Mask = mask; + public readonly Setting Value = new(value.Value & mask.Value); + + public MaskedSetting(ulong mask, ulong value) + : this(new Setting(mask), new Setting(value)) + { } + + public MaskedSetting Limit(int numOptions) + => new(Mask.Value & Setting.AllBits(numOptions).Value, Value.Value); + + public bool IsZero + => Mask.Value is 0; + + public bool IsEnabled(Setting input) + => (input.Value & Mask.Value) == Value.Value; +} From baca3cdec21a705f231d7d45edfcea8c17735602 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:08:09 +0200 Subject: [PATCH 014/108] Update Libs. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 2c3c32bf..ad3bafa4 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2c3c32bfb7057d7be7678f413122c2b1453050d5 +Subproject commit ad3bafa4f0af5b69833347f8c8ff2e178645e2f0 diff --git a/Penumbra.GameData b/Penumbra.GameData index 10fdb025..82b44672 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 10fdb025436f7ea9f1f5e97635c19eee0578de7b +Subproject commit 82b446721a9b9c99d2470c54ad49fe19ff4987e3 From 8527bfa29c6bc722d9dad579a1bf719700b9ccac Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 2 Aug 2025 00:13:35 +0200 Subject: [PATCH 015/108] Fix missing updates for OtterGui. --- Penumbra/Mods/Manager/ModFileSystem.cs | 16 ++++++++-------- Penumbra/Mods/SubMods/ComplexSubMod.cs | 2 -- Penumbra/UI/Tabs/SettingsTab.cs | 6 +++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index a5c46972..20a78995 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -37,11 +37,11 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer public struct ImportDate : ISortMode { - public string Name - => "Import Date (Older First)"; + public ReadOnlySpan Name + => "Import Date (Older First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate)); @@ -49,11 +49,11 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer public struct InverseImportDate : ISortMode { - public string Name - => "Import Date (Newer First)"; + public ReadOnlySpan Name + => "Import Date (Newer First)"u8; - public string Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."; + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8; public IEnumerable GetChildren(Folder f) => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate)); diff --git a/Penumbra/Mods/SubMods/ComplexSubMod.cs b/Penumbra/Mods/SubMods/ComplexSubMod.cs index 3eea6f15..7c189170 100644 --- a/Penumbra/Mods/SubMods/ComplexSubMod.cs +++ b/Penumbra/Mods/SubMods/ComplexSubMod.cs @@ -1,8 +1,6 @@ -using ImSharp; using Newtonsoft.Json.Linq; using OtterGui.Extensions; using Penumbra.Mods.Groups; -using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c1aea97c..2abf90ef 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -509,19 +509,19 @@ public class SettingsTab : ITab, IUiService { var sortMode = _config.SortMode; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); - using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) + using (var combo = ImUtf8.Combo("##sortMode", sortMode.Name)) { if (combo) foreach (var val in Configuration.Constants.ValidSortModes) { - if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + if (ImUtf8.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) { _config.SortMode = val; _selector.SetFilterDirty(); _config.Save(); } - ImGuiUtil.HoverTooltip(val.Description); + ImUtf8.HoverTooltip(val.Description); } } From 898963fea530a04799eeb84675354c520b19ecf5 Mon Sep 17 00:00:00 2001 From: Sebastina Date: Tue, 22 Jul 2025 14:08:30 -0500 Subject: [PATCH 016/108] Allow focusing a specified mod via HTTP API under the mods tab. --- Penumbra/Api/HttpApi.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index b6e1d799..8f8b44f4 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -19,6 +19,7 @@ public class HttpApi : IDisposable, IApiService [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 } @@ -115,6 +116,13 @@ 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().ConfigureAwait(false); + Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered."); + if (data.Path.Length != 0) + api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name); + } private record ModReloadData(string Path, string Name) { @@ -123,6 +131,13 @@ public class HttpApi : IDisposable, IApiService { } } + private record ModFocusData(string Path, string Name) + { + public ModFocusData() + : this(string.Empty, string.Empty) + { } + } + private record ModInstallData(string Path) { public ModInstallData() From f5f4fe7259cb8aeec36cad45e2bc342eda3f109f Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:07:22 +1000 Subject: [PATCH 017/108] Invalid tangent fix example --- Penumbra/Import/Models/Export/MeshExporter.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 0070a808..11c84677 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -340,6 +340,39 @@ public class MeshExporter return typeof(VertexPositionNormalTangent); } + + private const float UnitLengthThresholdVec3 = 0.00674f; + internal static bool _IsFinite(float value) + { + return float.IsFinite(value); + } + + internal static bool _IsFinite(Vector2 v) + { + return _IsFinite(v.X) && _IsFinite(v.Y); + } + + internal static bool _IsFinite(Vector3 v) + { + return _IsFinite(v.X) && _IsFinite(v.Y) && _IsFinite(v.Z); + } + internal static Boolean IsNormalized(Vector3 normal) + { + if (!_IsFinite(normal)) return false; + + return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3; + } + internal static Vector3 SanitizeNormal(Vector3 normal) + { + if (normal == Vector3.Zero) return Vector3.UnitX; + return IsNormalized(normal) ? normal : Vector3.Normalize(normal); + } + internal static Vector4 SanitizeTangent(Vector4 tangent) + { + var n = SanitizeNormal(new Vector3(tangent.X, tangent.Y, tangent.Z)); + var s = float.IsNaN(tangent.W) ? 1 : tangent.W; + return new Vector4(n, s > 0 ? 1 : -1); + } /// Build a geometry vertex from a vertex's attributes. private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) @@ -352,19 +385,21 @@ public class MeshExporter if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)) + SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; - + // var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; + var vec4 = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)); + var bitangent = vec4 with { W = vec4.W == 1 ? 1 : -1 }; + return new VertexPositionNormalTangent( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), - bitangent + SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))), + SanitizeTangent(bitangent) ); } From bdcab22a5528758d5f2d54505f3ecb5b866efb7d Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Fri, 1 Aug 2025 22:03:27 +1000 Subject: [PATCH 018/108] Cleanup methods to extension class --- Penumbra/Import/Models/Export/MeshExporter.cs | 43 ++---------- Penumbra/Import/Models/ModelExtensions.cs | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 Penumbra/Import/Models/ModelExtensions.cs diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 11c84677..2e41f65a 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -340,39 +340,6 @@ public class MeshExporter return typeof(VertexPositionNormalTangent); } - - private const float UnitLengthThresholdVec3 = 0.00674f; - internal static bool _IsFinite(float value) - { - return float.IsFinite(value); - } - - internal static bool _IsFinite(Vector2 v) - { - return _IsFinite(v.X) && _IsFinite(v.Y); - } - - internal static bool _IsFinite(Vector3 v) - { - return _IsFinite(v.X) && _IsFinite(v.Y) && _IsFinite(v.Z); - } - internal static Boolean IsNormalized(Vector3 normal) - { - if (!_IsFinite(normal)) return false; - - return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3; - } - internal static Vector3 SanitizeNormal(Vector3 normal) - { - if (normal == Vector3.Zero) return Vector3.UnitX; - return IsNormalized(normal) ? normal : Vector3.Normalize(normal); - } - internal static Vector4 SanitizeTangent(Vector4 tangent) - { - var n = SanitizeNormal(new Vector3(tangent.X, tangent.Y, tangent.Z)); - var s = float.IsNaN(tangent.W) ? 1 : tangent.W; - return new Vector4(n, s > 0 ? 1 : -1); - } /// Build a geometry vertex from a vertex's attributes. private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) @@ -385,21 +352,19 @@ public class MeshExporter if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)) ); if (_geometryType == typeof(VertexPositionNormalTangent)) { // (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range. // TODO: While this assumption is safe, it would be sensible to actually check. - // var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; - var vec4 = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)); - var bitangent = vec4 with { W = vec4.W == 1 ? 1 : -1 }; + var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; return new VertexPositionNormalTangent( ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), - SanitizeNormal(ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal))), - SanitizeTangent(bitangent) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), + bitangent.SanitizeTangent() ); } diff --git a/Penumbra/Import/Models/ModelExtensions.cs b/Penumbra/Import/Models/ModelExtensions.cs new file mode 100644 index 00000000..2edb3ca4 --- /dev/null +++ b/Penumbra/Import/Models/ModelExtensions.cs @@ -0,0 +1,69 @@ +namespace Penumbra.Import.Models; + +public static class ModelExtensions +{ + // https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158 + private const float UnitLengthThresholdVec3 = 0.00674f; + private const float UnitLengthThresholdVec4 = 0.00769f; + + internal static bool _IsFinite(this float value) + { + return float.IsFinite(value); + } + + internal static bool _IsFinite(this Vector2 v) + { + return v.X._IsFinite() && v.Y._IsFinite(); + } + + internal static bool _IsFinite(this Vector3 v) + { + return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite(); + } + + internal static bool _IsFinite(this in Vector4 v) + { + return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite(); + } + + internal static Boolean IsNormalized(this Vector3 normal) + { + if (!normal._IsFinite()) return false; + + return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3; + } + + internal static void ValidateNormal(this Vector3 normal, string msg) + { + if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid."); + + if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length."); + } + + internal static void ValidateTangent(this Vector4 tangent, string msg) + { + if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg); + + new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg); + } + + internal static Vector3 SanitizeNormal(this Vector3 normal) + { + if (normal == Vector3.Zero) return Vector3.UnitX; + return normal.IsNormalized() ? normal : Vector3.Normalize(normal); + } + + internal static bool IsValidTangent(this Vector4 tangent) + { + if (tangent.W != 1 && tangent.W != -1) return false; + + return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized(); + } + + internal static Vector4 SanitizeTangent(this Vector4 tangent) + { + var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal(); + var s = float.IsNaN(tangent.W) ? 1 : tangent.W; + return new Vector4(n, s > 0 ? 1 : -1); + } +} From 6689e326ee00d9dfddca4f813cb7232388cc0654 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Sat, 2 Aug 2025 00:27:21 +0200 Subject: [PATCH 019/108] Material tab: disallow "Enable Transparency" for stockings shader --- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 97acf130..77bfb795 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface.Components; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; @@ -119,11 +120,22 @@ public sealed partial class MtrlTab : IWritable, IDisposable using var dis = ImRaii.Disabled(disabled); var tmp = shaderFlags.EnableTransparency; - if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) + + // guardrail: the game crashes if transparency is enabled on characterstockings.shpk + var disallowTransparency = Mtrl.ShaderPackage.Name == "characterstockings.shpk"; + using (ImRaii.Disabled(disallowTransparency)) { - shaderFlags.EnableTransparency = tmp; - ret = true; - SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) + { + shaderFlags.EnableTransparency = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + } + + if (disallowTransparency) + { + ImGuiComponents.HelpMarker("Enabling transparency for shader package characterstockings.shpk will crash the game."); } ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); From 3f18ad50de0d3f360771fd472ef98a05008e5bd3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 00:45:24 +0200 Subject: [PATCH 020/108] Initial API13 / 7.3 update. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Api/IpcTester/CollectionsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/EditingIpcTester.cs | 2 +- Penumbra/Api/IpcTester/GameStateIpcTester.cs | 2 +- Penumbra/Api/IpcTester/IpcTester.cs | 2 +- Penumbra/Api/IpcTester/MetaIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ModSettingsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ModsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/PluginStateIpcTester.cs | 2 +- Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ResolveIpcTester.cs | 2 +- Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs | 2 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 4 ++-- Penumbra/Api/IpcTester/UiIpcTester.cs | 2 +- Penumbra/ChangedItemMode.cs | 2 +- Penumbra/CommandHandler.cs | 2 +- Penumbra/Import/TexToolsImporter.Gui.cs | 2 +- .../Textures/CombinedTexture.Manipulation.cs | 2 +- Penumbra/Import/Textures/TextureDrawer.cs | 4 ++-- Penumbra/Import/Textures/TextureManager.cs | 2 +- Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs | 2 +- Penumbra/Interop/MaterialPreview/MaterialInfo.cs | 2 +- Penumbra/Interop/ResourceTree/ResolveContext.cs | 4 ++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 5 ++--- Penumbra/Interop/Services/TextureArraySlicer.cs | 7 ++++--- Penumbra/Meta/ShapeAttributeManager.cs | 3 --- Penumbra/Mods/FeatureChecker.cs | 2 +- Penumbra/Penumbra.cs | 4 +++- Penumbra/Penumbra.csproj | 2 +- Penumbra/Penumbra.json | 2 +- Penumbra/Services/StainService.cs | 2 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../Materials/MaterialTemplatePickers.cs | 2 +- .../AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 2 +- .../Materials/MtrlTab.CommonColorTable.cs | 14 +++++++------- .../AdvancedWindow/Materials/MtrlTab.Constants.cs | 2 +- .../Materials/MtrlTab.LegacyColorTable.cs | 2 +- .../Materials/MtrlTab.LivePreview.cs | 2 +- .../Materials/MtrlTab.ShaderPackage.cs | 4 ++-- .../AdvancedWindow/Materials/MtrlTab.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 2 +- .../UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Deformers.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Materials.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.QuickImport.cs | 2 +- .../AdvancedWindow/ModEditWindow.ShaderPackages.cs | 4 ++-- .../UI/AdvancedWindow/ModEditWindow.Textures.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 2 +- Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs | 2 +- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 12 ++++++------ Penumbra/UI/Classes/CollectionSelectHeader.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 2 +- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 2 +- Penumbra/UI/CollectionTab/CollectionCombo.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 4 ++-- Penumbra/UI/CollectionTab/CollectionSelector.cs | 4 ++-- .../UI/CollectionTab/IndividualAssignmentUi.cs | 2 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 4 ++-- Penumbra/UI/ConfigWindow.cs | 2 +- Penumbra/UI/FileDialogService.cs | 2 +- Penumbra/UI/ImportPopup.cs | 2 +- Penumbra/UI/IncognitoService.cs | 2 +- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 2 +- Penumbra/UI/Knowledge/RaceCodeTab.cs | 2 +- Penumbra/UI/ModsTab/DescriptionEditPopup.cs | 2 +- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 2 +- .../ModsTab/Groups/CombiningModGroupEditDrawer.cs | 2 +- .../UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs | 2 +- .../UI/ModsTab/Groups/SingleModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanel.cs | 2 +- Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 10 +++++----- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelHeader.cs | 2 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 4 ++-- Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcherTable.cs | 2 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/ConfigTabBar.cs | 2 +- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 2 +- Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs | 3 +-- Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs | 2 +- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 2 +- Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs | 2 +- Penumbra/UI/Tabs/EffectiveTab.cs | 2 +- Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- Penumbra/UI/UiHelpers.cs | 12 ++++++------ 123 files changed, 158 insertions(+), 160 deletions(-) diff --git a/OtterGui b/OtterGui index ad3bafa4..9523b7ac 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ad3bafa4f0af5b69833347f8c8ff2e178645e2f0 +Subproject commit 9523b7ac725656b21fa98faef96962652e86e64f diff --git a/Penumbra.Api b/Penumbra.Api index ff7b3b40..c27a0600 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit ff7b3b4014a97455f823380c78b8a7c5107f8e2f +Subproject commit c27a06004138f2ec80ccdb494bb6ddf6d39d2165 diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index 4cb53c8b..abcb8e3d 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.GameData b/Penumbra.GameData index 82b44672..65c5bf3f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 82b446721a9b9c99d2470c54ad49fe19ff4987e3 +Subproject commit 65c5bf3f46569a54b0057c9015ab839b4e2a4350 diff --git a/Penumbra.String b/Penumbra.String index 0e5dcd1a..878acce4 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5 +Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 1d516eba..c06bdeb4 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -1,7 +1,7 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs index a1001630..d754cf90 100644 --- a/Penumbra/Api/IpcTester/EditingIpcTester.cs +++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs index 04541a57..38a09714 100644 --- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/IpcTester/IpcTester.cs b/Penumbra/Api/IpcTester/IpcTester.cs index 201e7068..b03d7e03 100644 --- a/Penumbra/Api/IpcTester/IpcTester.cs +++ b/Penumbra/Api/IpcTester/IpcTester.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using Penumbra.Api.Api; diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 9cf20cd7..bee1981c 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index c8eb8496..152efa45 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index a24861a3..9ea53366 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index a1bf4fc4..073305d0 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs index b862dde5..6b853ed2 100644 --- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs index a79b099d..9fc5bfc7 100644 --- a/Penumbra/Api/IpcTester/ResolveIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs @@ -1,5 +1,5 @@ using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.IpcSubscribers; diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs index 48f3b4a8..e6c8d52e 100644 --- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -1,8 +1,8 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index c106a867..64adf256 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; @@ -282,7 +282,7 @@ public class TemporaryIpcTester( foreach (var mod in list) { ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Name); + ImGui.TextUnformatted(mod.Name.Text); ImGui.TableNextColumn(); ImGui.TextUnformatted(mod.Priority.ToString()); ImGui.TableNextColumn(); diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index 647a4dda..852339c9 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -1,5 +1,5 @@ using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/ChangedItemMode.cs b/Penumbra/ChangedItemMode.cs index dccffded..ddb79ee0 100644 --- a/Penumbra/ChangedItemMode.cs +++ b/Penumbra/ChangedItemMode.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Text; namespace Penumbra; diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 9f681da2..b5d307ef 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -1,7 +1,7 @@ using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Classes; using OtterGui.Services; using Penumbra.Api.Api; diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index f145f560..5cb99d72 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.Import.Structs; diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 2d131d71..7a7e5888 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using SixLabors.ImageSharp.PixelFormats; diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index b0a65ac0..14203dff 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; @@ -20,7 +20,7 @@ public static class TextureDrawer { size = texture.TextureWrap.Size.Contain(size); - ImGui.Image(texture.TextureWrap.ImGuiHandle, size); + ImGui.Image(texture.TextureWrap.Handle, size); DrawData(texture); } else if (texture.LoadError != null) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 0c85f5be..073fef2f 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -406,7 +406,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) { - var device = uiBuilder.Device; + var device = new Device(uiBuilder.DeviceHandle); var dxgiDevice = device.QueryInterface(); using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 368845b4..523ae610 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); + Task = hooks.CreateHook("Change Customize", Sigs.UpdateDrawData, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); } public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index f2ea2d6c..a9fb46ff 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -85,7 +85,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy if (mtrlHandle == null) continue; - PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); + PathDataHandler.Split(mtrlHandle->FileName.AsSpan(), out var path, out _); var fileName = CiByteString.FromSpanUnsafe(path, true); if (fileName == needle) result.Add(new MaterialInfo(index, type, i, j)); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 64a91302..b2364e33 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -59,7 +59,7 @@ internal unsafe partial record ResolveContext( if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path)) return null; - return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); + return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, (ResourceHandle*)resourceHandle, path); } [SkipLocalsInit] @@ -245,7 +245,7 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached)) return cached; - var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); + var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, (ResourceHandle*)resource, path, false); var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value)); if (shpkNode is not null) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index e7c4b11b..49649e13 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -72,15 +72,14 @@ public class ResourceTree( var mpapArrayPtr = model->MaterialAnimationPacks; var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) var skinMtrlArray = modelType switch { - ModelType.Human => new ReadOnlySpan>((MaterialResourceHandle**)((nint)model + 0xB48), 5), + ModelType.Human => ((Human*) model)->SlotSkinMaterials, _ => [], }; var decalArray = modelType switch { - ModelType.Human => human->SlotDecalsSpan, + ModelType.Human => human->SlotDecals, ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals, ModelType.Weapon => [((Weapon*)model)->Decal], ModelType.Monster => [((Monster*)model)->Decal], diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index c934ac2b..11498878 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -1,3 +1,4 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using OtterGui.Services; using SharpDX.Direct3D; @@ -16,7 +17,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = []; /// Caching this across frames will cause a crash to desktop. - public nint GetImGuiHandle(Texture* texture, byte sliceIndex) + public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex) { if (texture == null) throw new ArgumentNullException(nameof(texture)); @@ -25,7 +26,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) { state.Refresh(); - return (nint)state.ShaderResourceView; + return new ImTextureID((nint)state.ShaderResourceView); } var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; var description = srv.Description; @@ -60,7 +61,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable } state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); _activeSlices.Add(((nint)texture, sliceIndex), state); - return (nint)state.ShaderResourceView; + return new ImTextureID((nint)state.ShaderResourceView); } public void Tick() diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index 16901741..a7f71ac7 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -1,4 +1,3 @@ -using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -6,8 +5,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; diff --git a/Penumbra/Mods/FeatureChecker.cs b/Penumbra/Mods/FeatureChecker.cs index 5800ef07..10874fc9 100644 --- a/Penumbra/Mods/FeatureChecker.cs +++ b/Penumbra/Mods/FeatureChecker.cs @@ -1,7 +1,7 @@ using System.Collections.Frozen; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Text; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index cf96c7f6..b22d049d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; +using Dalamud.Game; using OtterGui; using OtterGui.Log; using OtterGui.Services; @@ -20,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.Hooks; using Penumbra.Interop.Hooks.PostProcessing; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index c61692f4..3159b736 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 924d7bd3..bd9a2479 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 12, + "DalamudApiLevel": 13, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index b16d4dcd..17294aa8 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,7 +1,7 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Services; -using ImGuiNET; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c783e17f..a0305619 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Compression; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index f5d2a8c7..e9d76990 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 5c636b1d..241c3a91 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using FFXIVClientStructs.Interop; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index 0c987972..fad9adeb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index d70a4b50..39ff0a15 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files; using OtterGui.Text; @@ -338,10 +338,10 @@ public partial class MtrlTab var tmp = inputSqrt; if (ImUtf8.ColorEdit(label, ref tmp, ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.DisplayRgb + | ImGuiColorEditFlags.InputRgb | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR) + | ImGuiColorEditFlags.Hdr) && tmp != inputSqrt) { setter((HalfColor)PseudoSquareRgb(tmp)); @@ -373,10 +373,10 @@ public partial class MtrlTab var tmp = Vector4.Zero; ImUtf8.ColorEdit(label, ref tmp, ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.DisplayRgb + | ImGuiColorEditFlags.InputRgb | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR + | ImGuiColorEditFlags.Hdr | ImGuiColorEditFlags.AlphaPreview); if (letter.Length > 0 && ImGui.IsItemVisible()) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index f413a6a2..4ad6968b 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index 0ffdd1cc..bebacc94 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 5025bafd..dfa3a963 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index ee5341b2..43040ca3 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -384,7 +384,7 @@ public partial class MtrlTab var shpkFlags = (int)Mtrl.ShaderPackage.Flags; ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) return false; Mtrl.ShaderPackage.Flags = (uint)shpkFlags; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index ac88f77c..82ba7be4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 77bfb795..e15d1c90 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Components; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 5b6d585a..4a74cda5 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs index 89fadfa8..4b375c26 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 348a0d4c..16af5217 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index d6df95cb..77c2915a 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index e5e28a3d..84e09be5 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 929feadd..b03f4aa5 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 3691a4f7..4053560b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Structs; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 34488a87..bb87cd47 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 7e788462..f608a194 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index d60f877b..88abe0cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 35c8ccec..59692195 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 36154105..4f7ae8da 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 3f63967e..87d7487b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 4c946fe7..3caff226 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index aa3d9172..06cd0763 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index cc592296..a7db7f25 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 00caaabc..72350857 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Lumina.Data; using OtterGui.Text; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index a6a75e0d..baaf4a82 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; @@ -147,7 +147,7 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.MonoFont); var size = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20); - ImGuiNative.igInputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, + ImGuiNative.InputTextMultiline(DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, (uint)shader.Disassembly!.RawDisassembly.Length + 1, size, ImGuiInputTextFlags.ReadOnly, null, null); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index ee4e1eda..34e1e0d4 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e148167b..952d8489 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -4,7 +4,7 @@ using Dalamud.Interface.DragDrop; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Log; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 3c110fab..bf16fa37 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs index 1fa12b6d..c9996a1e 100644 --- a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs +++ b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 4d33a3fc..440baa2f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui; using OtterGui.Text; diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index a9070360..db54a8e5 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -3,7 +3,7 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; @@ -107,11 +107,11 @@ public class ChangedItemDrawer : IDisposable, IUiService return; } - ImGui.Image(icon.ImGuiHandle, new Vector2(height)); + ImGui.Image(icon.Handle, new Vector2(height)); if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.Image(icon.Handle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } @@ -193,7 +193,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); - ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, + ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].Handle, size, Vector2.Zero, Vector2.One, typeFilter switch { 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), @@ -213,7 +213,7 @@ public class ChangedItemDrawer : IDisposable, IUiService var localRet = false; var icon = _icons[type]; var flag = typeFilter.HasFlag(type); - ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); + ImGui.Image(icon.Handle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { typeFilter = flag ? typeFilter & ~type : typeFilter | type; @@ -232,7 +232,7 @@ public class ChangedItemDrawer : IDisposable, IUiService if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); + ImGui.Image(icon.Handle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index aa492362..355a6106 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 9c15ceb8..90ef0591 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Custom; namespace Penumbra.UI.Classes; diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index a3dcd23a..98a59a5b 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Services; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 98dc924f..bf97f178 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index dc0e71b5..26fa2b14 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -6,7 +6,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; @@ -346,7 +346,7 @@ public sealed class CollectionPanel( if (!source) return; - ImGui.SetDragDropPayload("DragIndividual", nint.Zero, 0); + ImGui.SetDragDropPayload("DragIndividual", null, 0); ImGui.TextUnformatted($"Re-ordering {text}..."); _draggedIndividualAssignment = _active.Individuals.Index(id); } diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 57429531..e54f994e 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; @@ -85,7 +85,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl if (source) { _dragging = Items[idx]; - ImGui.SetDragDropPayload(PayloadString, nint.Zero, 0); + ImGui.SetDragDropPayload(PayloadString, null, 0); ImGui.TextUnformatted($"Assigning {Name(_dragging)} to..."); } diff --git a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs index fd8f9b25..f472e346 100644 --- a/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs +++ b/Penumbra/UI/CollectionTab/IndividualAssignmentUi.cs @@ -1,5 +1,5 @@ using Dalamud.Game.ClientState.Objects.Enums; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Custom; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index cdc1e83e..2053f269 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; @@ -288,7 +288,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (!source) return; - ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0); + ImGui.SetDragDropPayload(InheritanceDragDropLabel, null, 0); _movedInheritance = collection; ImGui.TextUnformatted($"Moving {(_movedInheritance != null ? Name(_movedInheritance) : "Unknown")}..."); } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 64d370b5..55d0bc19 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 6773bc88..3bbc4ba8 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Services; diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 28767edc..59ed0308 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Import.Structs; diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index 29358618..678e072e 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Penumbra.UI.Classes; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index f831975b..118ed479 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.String; diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs index 36b048aa..44b544eb 100644 --- a/Penumbra/UI/Knowledge/RaceCodeTab.cs +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Text; using Penumbra.GameData.Enums; diff --git a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs index c284afc3..7d7a6967 100644 --- a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs +++ b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Mods; diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index a3e7ce14..1430f17b 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index 5bd5dfdf..e9840e6c 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 3d330093..fa5b0ef6 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 566ec02c..3d8409ad 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface.Components; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e9ab72ae..9610f173 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index 492a8fb7..8fa6a377 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8a383791..16ff7b41 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -2,7 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.DragDrop; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 9d6ead62..b7546699 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Services; using Penumbra.Mods; using Penumbra.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index b12df97d..332b64f0 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index ec020c86..70cad148 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; -using ImGuiNET; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index c750b8b0..1002d8ca 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Extensions; using OtterGui.Raii; @@ -73,7 +73,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy ImGui.TableNextColumn(); using var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderLine.Value()); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(selector.Selected!.Name); + ImGui.TextUnformatted(selector.Selected!.Name.Text); ImGui.TableNextColumn(); var actualSettings = collectionManager.Active.Current.GetActualSettings(selector.Selected!.Index).Settings!; var priority = actualSettings.Priority.Value; @@ -81,7 +81,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy using (ImRaii.Disabled(actualSettings is TemporaryModSettings)) { ImGui.SetNextItemWidth(priorityWidth); - if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) + if (ImGui.InputInt("##priority", ref priority, 0, 0, flags: ImGuiInputTextFlags.EnterReturnsTrue)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) @@ -104,7 +104,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy private void DrawConflictSelectable(ModConflicts conflict) { ImGui.AlignTextToFramePadding(); - if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) + if (ImGui.Selectable(conflict.Mod2.Name.Text) && conflict.Mod2 is Mod otherMod) selector.SelectByValue(otherMod); var hovered = ImGui.IsItemHovered(); var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); @@ -172,7 +172,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy var priority = _currentPriority ?? GetPriority(conflict).Value; ImGui.SetNextItemWidth(priorityWidth); - if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) + if (ImGui.InputInt("##priority", ref priority, 0, 0, flags: ImGuiInputTextFlags.EnterReturnsTrue)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 6fe3e4c6..71c1a225 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; -using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 478ab892..5b831a66 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; @@ -325,7 +325,7 @@ public class ModPanelEditTab( var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; ImGui.SetNextItemWidth(width); - if (ImGui.InputText(label, ref tmp, maxLength)) + if (ImGui.InputText(label, ref tmp)) { _currentEdit = tmp; _optionIndex = option; diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index aafbffa6..b42ac680 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -1,7 +1,7 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Plugin; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Communication; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 7c6ebf74..84f69bcb 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 639118f5..5981d979 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index ff5f636d..3eac972c 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; -using ImGuiNET; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 12355672..7e268e8c 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -1,6 +1,6 @@ -using Dalamud.Interface; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index d134cfe5..ee3613fc 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 009da842..97df095e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 6cee22d6..4dc9474f 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 05a1f33b..f2a041eb 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 28827ad9..43ae2488 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index 3b25c1a9..f136bacd 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Extensions; using OtterGui.Text; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 94c6cbd6..471d770a 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Raii; using Penumbra.CrashHandler; diff --git a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs index c8e7f001..672b8c79 100644 --- a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs +++ b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs @@ -1,6 +1,6 @@ using System.Text.Json; +using Dalamud.Bindings.ImGui; using Dalamud.Interface.DragDrop; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 76df5acc..eadee2d5 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -8,7 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index bfe89768..7af33a36 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -1,10 +1,9 @@ -using Dalamud.Hooking; +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Scheduler; using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; -using ImGuiNET; using OtterGui.Services; using OtterGui.Text; using Penumbra.Interop.Services; diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs index e8ff9b9c..f1024950 100644 --- a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Plugin; -using ImGuiNET; using OtterGui.Services; using OtterGui.Text; using Penumbra.Interop.Hooks; diff --git a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs index c8518315..e6e01107 100644 --- a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs +++ b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui.Services; using OtterGui.Text; using Penumbra.Services; diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs index c8c90e09..d497f90a 100644 --- a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using ImGuiNET; using OtterGui; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 7b940cd0..4c3b43bf 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs index 08d51184..4244e455 100644 --- a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs @@ -1,6 +1,6 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface.DragDrop; using Dalamud.Interface.Utility.Raii; -using ImGuiNET; using Lumina.Data.Files; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index ecf9a886..5691f821 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 613a3532..79dcbb9e 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -1,5 +1,5 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index c54e3433..593adde1 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -1,9 +1,9 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; -using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 2abf90ef..96d11baa 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,10 +1,10 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; -using ImGuiNET; using OtterGui; using OtterGui.Compression; using OtterGui.Custom; diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index deba7023..9fe90ee8 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; -using ImGuiNET; +using Dalamud.Bindings.ImGui; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -13,11 +13,11 @@ public static class UiHelpers { /// Draw text given by a ByteString. public static unsafe void Text(ByteString s) - => ImGuiNative.igTextUnformatted(s.Path, s.Path + s.Length); + => ImGuiNative.TextUnformatted(s.Path, s.Path + s.Length); /// Draw text given by a byte pointer and length. public static unsafe void Text(byte* s, int length) - => ImGuiNative.igTextUnformatted(s, s + length); + => ImGuiNative.TextUnformatted(s, s + length); /// Draw text given by a byte span. public static unsafe void Text(ReadOnlySpan s) @@ -36,7 +36,7 @@ public static class UiHelpers public static unsafe bool Selectable(ByteString s, bool selected) { var tmp = (byte)(selected ? 1 : 0); - return ImGuiNative.igSelectable_Bool(s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero) != 0; + return ImGuiNative.Selectable(s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero) != 0; } /// @@ -45,8 +45,8 @@ public static class UiHelpers /// public static unsafe void CopyOnClickSelectable(ByteString text) { - if (ImGuiNative.igSelectable_Bool(text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) != 0) - ImGuiNative.igSetClipboardText(text.Path); + if (ImGuiNative.Selectable(text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) != 0) + ImGuiNative.SetClipboardText(text.Path); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Click to copy to clipboard."); From a69811800d7203642f75b900bd56368199264283 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 15:56:25 +0200 Subject: [PATCH 021/108] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 65c5bf3f..ea49bc09 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 65c5bf3f46569a54b0057c9015ab839b4e2a4350 +Subproject commit ea49bc099e783ecafdf78f0bd0bc41fb8c60ad19 From 2b36f3984860a4db41cf2832c93f3e7220cd23f0 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Mon, 4 Aug 2025 18:37:36 +0200 Subject: [PATCH 022/108] Fix basecolor texture in material export --- Penumbra/Import/Models/Export/MaterialExporter.cs | 7 ++++--- .../AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 6be2ccbd..0d91534e 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -1,6 +1,7 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.UI.AdvancedWindow.Materials; using SharpGLTF.Materials; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; @@ -140,13 +141,13 @@ public class MaterialExporter // Lerp between table row values to fetch final pixel values for each subtexture. var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend); - baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); + baseColorSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedDiffuse), 1)); var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend); - specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); + specularSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedSpecularColor), 1)); var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend); - emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); + emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1)); } } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 39ff0a15..9ea9c2e0 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -594,7 +594,7 @@ public partial class MtrlTab internal static float PseudoSqrtRgb(float x) => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - internal static Vector3 PseudoSqrtRgb(Vector3 vec) + public static Vector3 PseudoSqrtRgb(Vector3 vec) => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); internal static Vector4 PseudoSqrtRgb(Vector4 vec) From 8140d085575aab238d5d844561afd8926d413d2d Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Mon, 4 Aug 2025 20:19:19 +0200 Subject: [PATCH 023/108] Add vertex material types for usages of 2 colour attributes --- Penumbra/Import/Models/Export/MeshExporter.cs | 83 +++- .../Import/Models/Export/VertexFragment.cs | 450 ++++++++++++++++++ 2 files changed, 523 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 2e41f65a..6ea2b284 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -390,23 +390,30 @@ public class MeshExporter } } + usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours); + var nColors = colours?.Count ?? 0; + var materialUsages = ( uvCount, - usages.ContainsKey(MdlFile.VertexUsage.Color) + nColors ); return materialUsages switch { - (3, true) => typeof(VertexTexture3ColorFfxiv), - (3, false) => typeof(VertexTexture3), - (2, true) => typeof(VertexTexture2ColorFfxiv), - (2, false) => typeof(VertexTexture2), - (1, true) => typeof(VertexTexture1ColorFfxiv), - (1, false) => typeof(VertexTexture1), - (0, true) => typeof(VertexColorFfxiv), - (0, false) => typeof(VertexEmpty), + (3, 2) => typeof(VertexTexture3Color2Ffxiv), + (3, 1) => typeof(VertexTexture3ColorFfxiv), + (3, 0) => typeof(VertexTexture3), + (2, 2) => typeof(VertexTexture2Color2Ffxiv), + (2, 1) => typeof(VertexTexture2ColorFfxiv), + (2, 0) => typeof(VertexTexture2), + (1, 2) => typeof(VertexTexture1Color2Ffxiv), + (1, 1) => typeof(VertexTexture1ColorFfxiv), + (1, 0) => typeof(VertexTexture1), + (0, 2) => typeof(VertexColor2Ffxiv), + (0, 1) => typeof(VertexColorFfxiv), + (0, 0) => typeof(VertexEmpty), - _ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), + _ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."), }; } @@ -419,6 +426,12 @@ public class MeshExporter if (_materialType == typeof(VertexColorFfxiv)) return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); + if (_materialType == typeof(VertexColor2Ffxiv)) + { + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1)); + } + if (_materialType == typeof(VertexTexture1)) return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); @@ -428,6 +441,16 @@ public class MeshExporter ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); + if (_materialType == typeof(VertexTexture1Color2Ffxiv)) + { + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + return new VertexTexture1Color2Ffxiv( + ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)), + ToVector4(color0), + ToVector4(color1) + ); + } + // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) @@ -448,6 +471,20 @@ public class MeshExporter ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } + + if (_materialType == typeof(VertexTexture2Color2Ffxiv)) + { + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + + return new VertexTexture2Color2Ffxiv( + new Vector2(uv.X, uv.Y), + new Vector2(uv.Z, uv.W), + ToVector4(color0), + ToVector4(color1) + ); + } + if (_materialType == typeof(VertexTexture3)) { // Not 100% sure about this @@ -472,6 +509,21 @@ public class MeshExporter ); } + if (_materialType == typeof(VertexTexture3Color2Ffxiv)) + { + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color); + + return new VertexTexture3Color2Ffxiv( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y), + ToVector4(color0), + ToVector4(color1) + ); + } + throw _notifier.Exception($"Unknown material type {_skinningType}"); } @@ -537,6 +589,17 @@ public class MeshExporter return list[0]; } + + /// Check that the list has length 2 for any case where this is expected and return both entries. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (T First, T Second) GetBothSafe(IReadOnlyDictionary> attributes, MdlFile.VertexUsage usage) + { + var list = attributes[usage]; + if (list.Count != 2) + throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2."); + + return (list[0], list[1]); + } /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 56495f2f..463c59fc 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -84,6 +84,103 @@ public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom } } +public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 0; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetTexCoord(int setIndex, Vector2 coord) + { } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} + + public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() @@ -172,6 +269,118 @@ public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : } } +public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 1; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex >= 1) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} + + public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() @@ -266,6 +475,124 @@ public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec } } +public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 2; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex >= 2) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } + +} + public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) : IVertexCustom { @@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } + +public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1) + : IVertexCustom +{ + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("TEXCOORD_1", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR_0", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + yield return new KeyValuePair("_FFXIV_COLOR_1", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector2 TexCoord2 = texCoord2; + public Vector4 FfxivColor0 = ffxivColor0; + public Vector4 FfxivColor1 = ffxivColor1; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 3; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public void Add(in VertexMaterialDelta delta) + { + TexCoord0 += delta.TexCoord0Delta; + TexCoord1 += delta.TexCoord1Delta; + TexCoord2 += delta.TexCoord2Delta; + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); + + public Vector2 GetTexCoord(int index) + => index switch + { + 0 => TexCoord0, + 1 => TexCoord1, + 2 => TexCoord2, + _ => throw new ArgumentOutOfRangeException(nameof(index)), + }; + + public void SetTexCoord(int setIndex, Vector2 coord) + { + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex == 1) + TexCoord1 = coord; + if (setIndex == 2) + TexCoord2 = coord; + if (setIndex >= 3) + throw new ArgumentOutOfRangeException(nameof(setIndex)); + } + + public bool TryGetCustomAttribute(string attributeName, out object? value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0": + value = FfxivColor0; + return true; + + case "_FFXIV_COLOR_1": + value = FfxivColor1; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + switch (attributeName) + { + case "_FFXIV_COLOR_0" when value is Vector4 valueVector4: + FfxivColor0 = valueVector4; + break; + case "_FFXIV_COLOR_1" when value is Vector4 valueVector4: + FfxivColor1 = valueVector4; + break; + } + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor0.X, + FfxivColor0.Y, + FfxivColor0.Z, + FfxivColor0.W, + }; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor0)); + components = + [ + FfxivColor1.X, + FfxivColor1.Y, + FfxivColor1.Z, + FfxivColor1.W, + ]; + if (components.Any(component => component is < 0 or > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor1)); + } +} From 93406e4d4e50e84b12292d74aeb5c1f0f697b8af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 16:17:59 +0200 Subject: [PATCH 024/108] 1.5.0.0 --- Penumbra/UI/Changelog.cs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index c1f7a1e6..4b487104 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -62,10 +62,37 @@ public class PenumbraChangelog : IUiService Add1_3_6_0(Changelog); Add1_3_6_4(Changelog); Add1_4_0_0(Changelog); + Add1_5_0_0(Changelog); } #region Changelogs + private static void Add1_5_0_0(Changelog log) + => log.NextVersion("Version 1.5.0.0") + .RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.") + .RegisterEntry("Added support for exporting models using two vertex color schemes (thanks zeroeightysix!).") + .RegisterEntry("Possibly improved the color accuracy of the basecolor texture created when exporting models (thanks zeroeightysix!).") + .RegisterEntry("Disabled enabling transparency for materials that use the characterstockings shader due to crashes (thanks zeroeightysix!).") + .RegisterEntry("Fixed some issues with model i/o and invalid tangents (thanks PassiveModding!)") + .RegisterEntry("Changed the behavior for default directory names when using the mod normalizer with combining groups.") + .RegisterEntry("Added jumping to specific mods to the HTTP API.") + .RegisterEntry("Fixed an issue with character sound modding (1.4.0.6).") + .RegisterHighlight("Added support for IMC-toggle attributes to accessories beyond the first toggle (1.4.0.5).") + .RegisterEntry("Fixed up some slot-specific attributes and shapes in models when swapping items between slots (1.4.0.5).") + .RegisterEntry("Added handling for human skin materials to the OnScreen tab and similar functionality (thanks Ny!) (1.4.0.5).") + .RegisterEntry("The OS thread ID a resource was loaded from was added to the resource logger (1.4.0.5).") + .RegisterEntry("A button linking to my (Ottermandias') Ko-Fi and Patreon was added in the settings tab. Feel free, but not pressured, to use it! :D ") + .RegisterHighlight("Mod setting combos now support mouse-wheel scrolling with Control and have filters (1.4.0.4).") + .RegisterEntry("Using the middle mouse button to toggle designs now works correctly with temporary settings (1.4.0.4).") + .RegisterEntry("Updated some BNPC associations (1.4.0.3).") + .RegisterEntry("Fixed further issues with shapes and attributes (1.4.0.4).") + .RegisterEntry("Penumbra now handles textures with MipMap offsets broken by TexTools on import and removes unnecessary MipMaps (1.4.0.3).") + .RegisterEntry("Updated the Mod Merger for the new group types (1.4.0.3).") + .RegisterEntry("Added querying Penumbra for supported features via IPC (1.4.0.3).") + .RegisterEntry("Shape names can now be edited in Penumbras model editor (1.4.0.2).") + .RegisterEntry("Attributes and Shapes can be fully toggled (1.4.0.2).") + .RegisterEntry("Fixed several issues with attributes and shapes (1.4.0.1)."); + private static void Add1_4_0_0(Changelog log) => log.NextVersion("Version 1.4.0.0") .RegisterHighlight("Added two types of new Meta Changes, SHP and ATR (Thanks Karou!).") From 13df8b2248010554342a4081ac27ad4fd6d67471 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:02:22 +0200 Subject: [PATCH 025/108] Update gamedata. --- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ea49bc09..fd875c43 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ea49bc099e783ecafdf78f0bd0bc41fb8c60ad19 +Subproject commit fd875c43ee910350107b2609809335285bd4ac0f diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index eadee2d5..9356ff5e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -726,6 +726,9 @@ public class DebugTab : Window, ITab, IUiService if (agent->Data == null) agent = &AgentBannerMIP.Instance()->AgentBannerInterface; + ImUtf8.Text("Agent: "); + ImGui.SameLine(0, 0); + Penumbra.Dynamis.DrawPointer((nint)agent); if (agent->Data != null) { using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); From bedfb22466801e3a9bf8503a6fe0745ce1766a30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:04:50 +0200 Subject: [PATCH 026/108] Use staging for release. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c87c0244..377919b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | From 13283c969015d8e1d89a37721e6f5bc54da32664 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:08:26 +0200 Subject: [PATCH 027/108] Fix dumb. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9356ff5e..b9e36d80 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1048,7 +1048,7 @@ public class DebugTab : Window, ITab, IUiService if (t1) { ImGuiUtil.DrawTableColumn("Flags"); - ImGuiUtil.DrawTableColumn($"{model->UnkFlags_01:X2}"); + ImGuiUtil.DrawTableColumn($"{model->StateFlags}"); ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded"); ImGuiUtil.DrawTableColumn($"{model->HasModelInSlotLoaded:X8}"); ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded"); From 66543cc671f77d14d21dc3b0c5a1df644799e638 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 8 Aug 2025 21:12:00 +0000 Subject: [PATCH 028/108] [CI] Updating repo.json for 1.5.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index af368d75..16206811 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.4.0.1", - "TestingAssemblyVersion": "1.4.0.6", + "AssemblyVersion": "1.5.0.0", + "TestingAssemblyVersion": "1.5.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 12, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 46cfbcb115f34d4e5d5e2203b41e0379223e679b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 Aug 2025 23:13:23 +0200 Subject: [PATCH 029/108] Set Repo API level to 13 and remove stg from future releases. --- .github/workflows/release.yml | 2 +- repo.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 377919b2..c87c0244 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | diff --git a/repo.json b/repo.json index 16206811..8cc42d45 100644 --- a/repo.json +++ b/repo.json @@ -9,8 +9,8 @@ "TestingAssemblyVersion": "1.5.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 12, - "TestingDalamudApiLevel": 12, + "DalamudApiLevel": 13, + "TestingDalamudApiLevel": 13, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 11cd08a9dec41d90b1e40f70e2c60d7508a4c7bf Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 Aug 2025 03:31:17 +0200 Subject: [PATCH 030/108] ClientStructs-ify stuff --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 3 +-- Penumbra/Interop/Structs/StructExtensions.cs | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 49649e13..ddef347d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -258,8 +258,7 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1475) - var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; + var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null; if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 62dca02e..031d24b1 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -36,10 +36,8 @@ internal static class StructExtensions public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1474) - var vf91 = (delegate* unmanaged)((nint*)character.VirtualTable)[91]; var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(vf91((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, slotIndex)); + return ToOwnedByteString(character.ResolveSkinMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex)); } public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) From 6242b30f93b1992b2639c3180a77aa333a365472 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 11:58:28 +0200 Subject: [PATCH 031/108] Fix resizable child. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 9523b7ac..539ce9e5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9523b7ac725656b21fa98faef96962652e86e64f +Subproject commit 539ce9e504fdc8bb0c2ca229905f4d236c376f6a From ff2b2be95352191c50701d5634c28fb75b53c000 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 12:11:29 +0200 Subject: [PATCH 032/108] Fix popups not working early. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 539ce9e5..5224ac53 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 539ce9e504fdc8bb0c2ca229905f4d236c376f6a +Subproject commit 5224ac538b1a7c0e86e7d2ceaf652d8d807888ae From 391c9d727e2946e1a55bbda60cb238e2adde733a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 12:51:39 +0200 Subject: [PATCH 033/108] Fix shifted timeline vfunc offset. --- Penumbra/Interop/GameState.cs | 3 --- Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 32b45b7e..b5171244 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -59,9 +59,6 @@ public class GameState : IService private readonly ThreadLocal _characterSoundData = new(() => ResolveData.Invalid, true); - public ResolveData SoundData - => _characterSoundData.Value; - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public ResolveData SetSoundData(ResolveData data) { diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index cdd82b95..e0eb7ec5 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -63,7 +63,8 @@ public sealed unsafe class LoadTimelineResources : FastHookGetOwningGameObjectIndex(); + // TODO: Clientstructify + var idx = ((delegate* unmanaged**)timeline)[0][29](timeline); if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; From 02af52671f0f5ce8ff8293bbcfe1fc2f0419a277 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 9 Aug 2025 13:00:40 +0200 Subject: [PATCH 034/108] Need staging again ... --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c87c0244..377919b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: run: dotnet restore - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev" - name: Build run: | From 3785a629ce42c500f604d85b78cd67ea03e7e6d6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 9 Aug 2025 11:03:24 +0000 Subject: [PATCH 035/108] [CI] Updating repo.json for 1.5.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 8cc42d45..9a970ea7 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.0", - "TestingAssemblyVersion": "1.5.0.0", + "AssemblyVersion": "1.5.0.1", + "TestingAssemblyVersion": "1.5.0.1", "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.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9aae2210a2f7194a61da4844f7c8df5812fbfa73 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 Aug 2025 14:54:56 +0200 Subject: [PATCH 036/108] Fix nullptr crashes --- OtterGui | 2 +- .../UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 5224ac53..0eaf7655 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5224ac538b1a7c0e86e7d2ceaf652d8d807888ae +Subproject commit 0eaf7655123bd6502456e93d6ae9593249d3f792 diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index bebacc94..e75cd633 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -67,7 +67,7 @@ public partial class MtrlTab private static void DrawLegacyColorTableHeader(bool hasDyeTable) { ImGui.TableNextColumn(); - ImUtf8.TableHeader(default(ReadOnlySpan)); + ImUtf8.TableHeader(""u8); ImGui.TableNextColumn(); ImUtf8.TableHeader("Row"u8); ImGui.TableNextColumn(); From 155d3d49aa640da456794d9756097e82de158417 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 9 Aug 2025 16:40:42 +0000 Subject: [PATCH 037/108] [CI] Updating repo.json for 1.5.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 9a970ea7..cd2a8018 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.1", - "TestingAssemblyVersion": "1.5.0.1", + "AssemblyVersion": "1.5.0.2", + "TestingAssemblyVersion": "1.5.0.2", "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.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f6bac93db78f1dc42ea48774a37c38c07b2460dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 11 Aug 2025 19:58:24 +0200 Subject: [PATCH 038/108] Update ChangedEquipData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fd875c43..2f5e9013 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fd875c43ee910350107b2609809335285bd4ac0f +Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a From 12a218bb2b4cee341d31fa3d2887b76ec67b816c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Aug 2025 12:28:56 +0200 Subject: [PATCH 039/108] Protect against empty requested paths. --- .../Hooks/ResourceLoading/ResourceService.cs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index e90b4575..1a40accc 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -85,7 +86,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8); private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, uint unk9); + ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, + uint unk9); [Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))] private readonly Hook _getResourceSyncHook = null!; @@ -118,18 +120,26 @@ public unsafe class ResourceService : IDisposable, IRequiredService unk9); } - var original = gamePath; + if (gamePath.IsEmpty) + { + Penumbra.Log.Error($"[ResourceService] Empty resource path requested with category {*categoryId}, type {*resourceType}, hash {*resourceHash}."); + return null; + } + + var original = gamePath; ResourceHandle* returnValue = null; ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync, ref returnValue); if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, unk9); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, + unk9); } /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original, + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, + Utf8GamePath original, GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) { var previous = _currentGetResourcePath.Value; @@ -141,7 +151,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService resourceParameters, unk8, unk9) : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, resourceParameters, unk, unk8, unk9); - } finally + } + finally { _currentGetResourcePath.Value = previous; } @@ -163,7 +174,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService /// The original game path of the resource, if loaded synchronously. /// The previous state of the resource. /// The return value to use. - public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); + public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, + (byte UnkState, LoadState LoadState) previousState, ref uint returnValue); /// /// @@ -185,7 +197,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) { var previousState = (handle->UnkState, handle->LoadState); - var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty; + var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty; ResourceStateUpdating?.Invoke(handle, syncOriginal); var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread); ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret); From 7af81a6c18f382cd2b2cf806134060fed421dbd9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Aug 2025 12:29:09 +0200 Subject: [PATCH 040/108] Fix issue with removing default metadata. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index ede062ae..c2c9e777 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -126,6 +126,7 @@ public class MetaDictionary { _data = null; Count = 0; + return; } Count = GlobalEqp.Count + Shp.Count + Atr.Count; From b112d75a27baac5ea02e60c353fcb32e6f46e609 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 12 Aug 2025 10:31:13 +0000 Subject: [PATCH 041/108] [CI] Updating repo.json for 1.5.0.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index cd2a8018..305912c0 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.2", - "TestingAssemblyVersion": "1.5.0.2", + "AssemblyVersion": "1.5.0.3", + "TestingAssemblyVersion": "1.5.0.3", "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.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9f8185f67baf18ee552bf68fa3556fa4509d3acb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 12 Aug 2025 14:47:35 +0200 Subject: [PATCH 042/108] Add new parameter to LoadWeapon hook. --- Penumbra/Interop/Hooks/Objects/WeaponReload.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index b09103f6..4231b027 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -35,14 +35,14 @@ public sealed unsafe class WeaponReload : EventWrapperPtr _task.IsCompletedSuccessfully; - private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g); + private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h); - private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g) + private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h) { var gameObject = drawData->OwnerObject; - Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); + Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}, {h}."); Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); - _task.Result.Original(drawData, slot, weapon, d, e, f, g); + _task.Result.Original(drawData, slot, weapon, d, e, f, g, h); _postEvent.Invoke(drawData, gameObject); } From 9aff388e21a6e0d688157123ec5b3d2d061b90dd Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 12 Aug 2025 12:53:33 +0000 Subject: [PATCH 043/108] [CI] Updating repo.json for 1.5.0.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 305912c0..6045e266 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.3", - "TestingAssemblyVersion": "1.5.0.3", + "AssemblyVersion": "1.5.0.4", + "TestingAssemblyVersion": "1.5.0.4", "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.0.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a7246b9d98392db549ec6fd408d430843664e48b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Aug 2025 16:50:26 +0200 Subject: [PATCH 044/108] Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../PostProcessing/PreBoneDeformerReplacer.cs | 2 +- .../Processing/PbdFilePostProcessor.cs | 119 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/FileEditor.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 16 +-- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 73 ++++++----- Penumbra/packages.lock.json | 22 +++- 8 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 Penumbra/Interop/Processing/PbdFilePostProcessor.cs diff --git a/OtterGui b/OtterGui index 0eaf7655..3ea61642 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0eaf7655123bd6502456e93d6ae9593249d3f792 +Subproject commit 3ea61642a05403fb2b64032112ff674b387825b3 diff --git a/Penumbra.GameData b/Penumbra.GameData index 2f5e9013..2cf59c61 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a +Subproject commit 2cf59c61494a01fd14aecf925e7dc6325a7374ac diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 30e643c7..51af5813 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); - + var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { diff --git a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs new file mode 100644 index 00000000..69f2ecd5 --- /dev/null +++ b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs @@ -0,0 +1,119 @@ +using Dalamud.Game; +using Dalamud.Plugin.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class PbdFilePostProcessor : IFilePostProcessor +{ + private readonly IFileAllocator _allocator; + private byte[] _epbdData; + private unsafe delegate* unmanaged _loadEpbdData; + + public ResourceType Type + => ResourceType.Pbd; + + public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner) + { + _allocator = allocator; + _epbdData = SetEpbdData(dataManager); + _loadEpbdData = (delegate* unmanaged)scanner.ScanText(Sigs.LoadEpbdData); + } + + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) + { + if (_epbdData.Length is 0) + return; + + if (resource->LoadState is not LoadState.Success) + { + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} failed load ({resource->LoadState})."); + return; + } + + var (data, length) = resource->GetData(); + if (length is 0 || data == nint.Zero) + { + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} succeeded load but has no data."); + return; + } + + var span = new ReadOnlySpan((void*)data, (int)resource->FileSize); + var reader = new PackReader(span); + if (reader.HasData) + { + Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {originalGamePath} with EPBD data."); + return; + } + + var newData = AppendData(span); + fixed (byte* ptr = newData) + { + // Set the appended data and the actual file size, then re-load the EPBD data via game function call. + if (resource->SetData((nint)ptr, newData.Length)) + { + resource->FileSize = (uint)newData.Length; + resource->CsHandle.FileSize2 = (uint)newData.Length; + resource->CsHandle.FileSize3 = (uint)newData.Length; + _loadEpbdData(resource); + // Free original data. + _allocator.Release((void*)data, length); + Penumbra.Log.Verbose($"[ResourceLoader] Loaded {originalGamePath} from file and appended default EPBD data."); + } + else + { + Penumbra.Log.Warning( + $"[ResourceLoader] Failed to append EPBD data to custom PBD at {originalGamePath}."); + } + } + } + + /// Combine the given data with the default PBD data using the game's file allocator. + private unsafe ReadOnlySpan AppendData(ReadOnlySpan data) + { + // offset has to be set, otherwise not called. + var newLength = data.Length + _epbdData.Length; + var memory = _allocator.Allocate(newLength); + var span = new Span(memory, newLength); + data.CopyTo(span); + _epbdData.CopyTo(span[data.Length..]); + return span; + } + + /// Fetch the default EPBD data from the .pbd file of the game's installation. + private static byte[] SetEpbdData(IDataManager dataManager) + { + try + { + var file = dataManager.GetFile(GamePaths.Pbd.Path); + if (file is null || file.Data.Length is 0) + { + Penumbra.Log.Warning("Default PBD file has no data."); + return []; + } + + ReadOnlySpan span = file.Data; + var reader = new PackReader(span); + if (!reader.HasData) + { + Penumbra.Log.Warning("Default PBD file has no EPBD section."); + return []; + } + + var offset = span.Length - (int)reader.PackLength; + var ret = span[offset..]; + Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}."); + return ret.ToArray(); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}"); + return []; + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index a0305619..424bc56f 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -8,6 +8,7 @@ using OtterGui.Compression; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; +using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -80,7 +81,7 @@ public class FileEditor( private Exception? _currentException; private bool _changed; - private string _defaultPath = string.Empty; + private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty; private bool _inInput; private Utf8GamePath _defaultPathUtf8; private bool _isDefaultPathUtf8Valid; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b9e36d80..d41dd25a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1236,16 +1236,12 @@ public class DebugTab : Window, ITab, IUiService } public static unsafe void DrawCopyableAddress(ReadOnlySpan label, void* address) - { - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) - { - if (ImUtf8.Selectable($"0x{(nint)address:X16} {label}")) - ImUtf8.SetClipboardText($"0x{(nint)address:X16}"); - } - - ImUtf8.HoverTooltip("Click to copy address to clipboard."u8); - } + => DrawCopyableAddress(label, (nint)address); public static unsafe void DrawCopyableAddress(ReadOnlySpan label, nint address) - => DrawCopyableAddress(label, (void*)address); + { + Penumbra.Dynamis.DrawPointer(address); + ImUtf8.SameLineInner(); + ImUtf8.Text(label); + } } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 7af33a36..f0ab1125 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -27,14 +27,31 @@ public unsafe class GlobalVariablesDrawer( return; var actionManager = (ActionTimelineManager**)ActionTimelineManager.Instance(); - DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); - DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); - DebugTab.DrawCopyableAddress("ScheduleManagement"u8, ScheduleManagement.Instance()); - DebugTab.DrawCopyableAddress("ActionTimelineManager*"u8, actionManager); - DebugTab.DrawCopyableAddress("ActionTimelineManager"u8, actionManager != null ? *actionManager : null); - DebugTab.DrawCopyableAddress("SchedulerResourceManagement*"u8, scheduler.Address); - DebugTab.DrawCopyableAddress("SchedulerResourceManagement"u8, scheduler.Address != null ? *scheduler.Address : null); - DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); + using (ImUtf8.Group()) + { + Penumbra.Dynamis.DrawPointer(characterUtility.Address); + Penumbra.Dynamis.DrawPointer(residentResources.Address); + Penumbra.Dynamis.DrawPointer(ScheduleManagement.Instance()); + Penumbra.Dynamis.DrawPointer(actionManager); + Penumbra.Dynamis.DrawPointer(actionManager != null ? *actionManager : null); + Penumbra.Dynamis.DrawPointer(scheduler.Address); + Penumbra.Dynamis.DrawPointer(scheduler.Address != null ? *scheduler.Address : null); + Penumbra.Dynamis.DrawPointer(Device.Instance()); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text("CharacterUtility"u8); + ImUtf8.Text("ResidentResourceManager"u8); + ImUtf8.Text("ScheduleManagement"u8); + ImUtf8.Text("ActionTimelineManager*"u8); + ImUtf8.Text("ActionTimelineManager"u8); + ImUtf8.Text("SchedulerResourceManagement*"u8); + ImUtf8.Text("SchedulerResourceManagement"u8); + ImUtf8.Text("Device"u8); + } + DrawCharacterUtility(); DrawResidentResources(); DrawSchedulerResourcesMap(); @@ -63,7 +80,7 @@ public unsafe class GlobalVariablesDrawer( var resource = characterUtility.Address->Resource(idx); ImUtf8.DrawTableColumn($"[{idx}]"); ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); if (resource == null) { ImGui.TableNextRow(); @@ -74,25 +91,12 @@ public unsafe class GlobalVariablesDrawer( ImGui.TableNextColumn(); var data = (nint)resource->CsHandle.GetData(); var length = resource->CsHandle.GetLength(); - if (ImUtf8.Selectable($"0x{data:X}")) - if (data != nint.Zero && length > 0) - ImUtf8.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - - ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + Penumbra.Dynamis.DrawPointer(data); ImUtf8.DrawTableColumn(length.ToString()); - ImGui.TableNextColumn(); if (intern.Value != -1) { - ImUtf8.Selectable($"0x{characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImUtf8.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)characterUtility.DefaultResource(intern).Address, - characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - - ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); - + Penumbra.Dynamis.DrawPointer(characterUtility.DefaultResource(intern).Address); ImUtf8.DrawTableColumn($"{characterUtility.DefaultResource(intern).Size}"); } else @@ -122,7 +126,7 @@ public unsafe class GlobalVariablesDrawer( var resource = residentResources.Address->ResourceList[idx]; ImUtf8.DrawTableColumn($"[{idx}]"); ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); if (resource == null) { ImGui.TableNextRow(); @@ -133,12 +137,7 @@ public unsafe class GlobalVariablesDrawer( ImGui.TableNextColumn(); var data = (nint)resource->CsHandle.GetData(); var length = resource->CsHandle.GetLength(); - if (ImUtf8.Selectable($"0x{data:X}")) - if (data != nint.Zero && length > 0) - ImUtf8.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - - ImUtf8.HoverTooltip("Click to copy bytes to clipboard."u8); + Penumbra.Dynamis.DrawPointer(data); ImUtf8.DrawTableColumn(length.ToString()); } } @@ -184,15 +183,15 @@ public unsafe class GlobalVariablesDrawer( ImUtf8.DrawTableColumn($"{resource->Consumers}"); ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); var resourceHandle = *((ResourceHandle**)resource + 3); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + Penumbra.Dynamis.DrawPointer(resourceHandle); ImGui.TableNextColumn(); ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); ImGui.TableNextColumn(); uint dataLength = 0; - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength)); ImUtf8.DrawTableColumn($"{dataLength}"); ++_shownResourcesMap; } @@ -233,15 +232,15 @@ public unsafe class GlobalVariablesDrawer( ImUtf8.DrawTableColumn($"{resource->Consumers}"); ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); var resourceHandle = *((ResourceHandle**)resource + 3); - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + Penumbra.Dynamis.DrawPointer(resourceHandle); ImGui.TableNextColumn(); ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); ImGui.TableNextColumn(); uint dataLength = 0; - ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength)); ImUtf8.DrawTableColumn($"{dataLength}"); ++_shownResourcesList; } diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 778f776e..7499bffa 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -58,6 +58,19 @@ "resolved": "3.1.11", "contentHash": "JfPLyigLthuE50yi6tMt7Amrenr/fA31t2CvJyhy/kQmfulIBAqo5T/YFUSRHtuYPXRSaUHygFeh6Qd933EoSw==" }, + "FlatSharp.Compiler": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A==" + }, + "FlatSharp.Runtime": { + "type": "Transitive", + "resolved": "7.9.0", + "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==", + "dependencies": { + "System.Memory": "4.5.5" + } + }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2024.3.0", @@ -94,6 +107,11 @@ "resolved": "4.6.0", "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" + }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", "resolved": "8.0.1", @@ -133,8 +151,10 @@ "penumbra.gamedata": { "type": "Project", "dependencies": { + "FlatSharp.Compiler": "[7.9.0, )", + "FlatSharp.Runtime": "[7.9.0, )", "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.6.1, )", + "Penumbra.Api": "[5.10.0, )", "Penumbra.String": "[1.0.6, )" } }, From f69c2643176b2c2b08eb92e08facc945f96b3ce3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 13 Aug 2025 14:53:11 +0000 Subject: [PATCH 045/108] [CI] Updating repo.json for 1.5.0.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 6045e266..f6d69c8b 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.4", - "TestingAssemblyVersion": "1.5.0.4", + "AssemblyVersion": "1.5.0.5", + "TestingAssemblyVersion": "1.5.0.5", "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.0.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5917f5fad12125339d4cb52182dfc57bdbdbbf80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 13 Aug 2025 17:42:40 +0200 Subject: [PATCH 046/108] Small fixes. --- Penumbra/Interop/Processing/PbdFilePostProcessor.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs index 69f2ecd5..674500cd 100644 --- a/Penumbra/Interop/Processing/PbdFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/PbdFilePostProcessor.cs @@ -32,14 +32,14 @@ public sealed class PbdFilePostProcessor : IFilePostProcessor if (resource->LoadState is not LoadState.Success) { - Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} failed load ({resource->LoadState})."); + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} failed load ({resource->LoadState})."); return; } var (data, length) = resource->GetData(); if (length is 0 || data == nint.Zero) { - Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {originalGamePath} succeeded load but has no data."); + Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} succeeded load but has no data."); return; } @@ -47,7 +47,7 @@ public sealed class PbdFilePostProcessor : IFilePostProcessor var reader = new PackReader(span); if (reader.HasData) { - Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {originalGamePath} with EPBD data."); + Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {resource->FileName()} with EPBD data."); return; } @@ -63,12 +63,12 @@ public sealed class PbdFilePostProcessor : IFilePostProcessor _loadEpbdData(resource); // Free original data. _allocator.Release((void*)data, length); - Penumbra.Log.Verbose($"[ResourceLoader] Loaded {originalGamePath} from file and appended default EPBD data."); + Penumbra.Log.Debug($"[ResourceLoader] Loaded {resource->FileName()} from file and appended default EPBD data."); } else { Penumbra.Log.Warning( - $"[ResourceLoader] Failed to append EPBD data to custom PBD at {originalGamePath}."); + $"[ResourceLoader] Failed to append EPBD data to custom PBD at {resource->FileName()}."); } } } From 87ace28bcfea13d8a50c8f8c1e3822a2f0302353 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 16 Aug 2025 11:56:24 +0200 Subject: [PATCH 047/108] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 3ea61642..4a9b71a9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3ea61642a05403fb2b64032112ff674b387825b3 +Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89 From aa920b5e9b46f7139decfea27d248478212012fa Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 17 Aug 2025 01:41:49 +0200 Subject: [PATCH 048/108] Fix ImGui texture usage issue --- Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 241c3a91..24a5f9c2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -131,7 +131,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService if (texture == null) continue; var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); - if (handle == 0) + if (handle.IsNull) continue; var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; From 41edc2382005e3ff2600872aefb276fabd741c30 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 17 Aug 2025 03:10:35 +0200 Subject: [PATCH 049/108] Allow changing the skin mtrl suffix --- .../Hooks/Resources/ResolvePathHooksBase.cs | 9 ++- .../Processing/SkinMtrlPathEarlyProcessing.cs | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 85fb1098..eecb98c5 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -6,6 +6,7 @@ using Penumbra.Collections; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Processing; using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; namespace Penumbra.Interop.Hooks.Resources; @@ -159,7 +160,13 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable => ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName)); private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) - => ResolvePath(drawObject, _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + { + var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex); + if (finalPathBuffer != 0 && finalPathBuffer == pathBuffer) + SkinMtrlPathEarlyProcessing.Process(new Span((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex); + + return ResolvePath(drawObject, finalPathBuffer); + } private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs new file mode 100644 index 00000000..d35845e1 --- /dev/null +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -0,0 +1,61 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Processing; + +public static unsafe class SkinMtrlPathEarlyProcessing +{ + public static void Process(Span path, CharacterBase* character, uint slotIndex) + { + var end = path.IndexOf(".mtrl\0"u8); + if (end < 0) + return; + + var suffixPos = path[..end].LastIndexOf((byte)'_'); + if (suffixPos < 0) + return; + + var handle = GetModelResourceHandle(character, slotIndex); + if (handle == null) + return; + + var skinSuffix = GetSkinSuffix(handle); + if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7) + return; + + skinSuffix.CopyTo(path[(suffixPos + 1)..]); + ".mtrl\0"u8.CopyTo(path[(suffixPos + 1 + skinSuffix.Length)..]); + } + + private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex) + { + if (character == null) + return null; + + if (character->TempSlotData != null) + { + // TODO ClientStructs-ify + var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); + if (handle != null) + return handle; + } + + var model = character->Models[slotIndex]; + if (model == null) + return null; + + return model->ModelResourceHandle; + } + + private static ReadOnlySpan GetSkinSuffix(ModelResourceHandle* handle) + { + foreach (var (attribute, _) in handle->Attributes) + { + var attributeSpan = attribute.AsSpan(); + if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_') + return attributeSpan[12..]; + } + + return []; + } +} From 24cbc6c5e141aacddda897dc3e0a2eb637847f99 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 17 Aug 2025 08:46:26 +0000 Subject: [PATCH 050/108] [CI] Updating repo.json for 1.5.0.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f6d69c8b..a452dc94 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.5", - "TestingAssemblyVersion": "1.5.0.5", + "AssemblyVersion": "1.5.0.6", + "TestingAssemblyVersion": "1.5.0.6", "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.0.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.5/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.0.6/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" } ] From 8304579d29788d4bb41b7af043516e3912561d86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Aug 2025 13:54:31 +0200 Subject: [PATCH 051/108] Add predefined tags to the multi mod selector. --- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 22 +++- Penumbra/UI/PredefinedTagManager.cs | 119 ++++++++++++++++-- 4 files changed, 132 insertions(+), 13 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 71c1a225..b8710707 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -30,7 +30,7 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Count > 0 + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 5b831a66..c3737b40 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -69,7 +69,7 @@ public class ModPanelEditTab( FeatureChecker.DrawFeatureFlagInput(modManager.DataEditor, _mod, UiHelpers.InputTextWidth.X); UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = predefinedTagManager.Count > 0; + var sharedTagsEnabled = predefinedTagManager.Enabled; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 3eac972c..947ede14 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; using OtterGui.Extensions; +using OtterGui.Filesystem; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -10,7 +11,7 @@ using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) : IUiService +public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, PredefinedTagManager tagManager) : IUiService { public void Draw() { @@ -97,7 +98,12 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) var width = ImGuiHelpers.ScaledVector2(150, 0); ImUtf8.TextFrameAligned("Multi Tagger:"u8); ImGui.SameLine(); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); + + var predefinedTagsEnabled = tagManager.Enabled; + var inputWidth = predefinedTagsEnabled + ? ImGui.GetContentRegionAvail().X - 2 * width.X - 3 * ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() + : ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.SetNextItemWidth(inputWidth); ImUtf8.InputText("##tag"u8, ref _tag, "Local Tag Name..."u8); UpdateTagCache(); @@ -109,7 +115,7 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) ? "No tag specified." : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data." : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name.Text))}"; - ImGui.SameLine(); + ImUtf8.SameLineInner(); if (ImUtf8.ButtonEx(label, tooltip, width, _addMods.Count == 0)) foreach (var mod in _addMods) editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); @@ -122,10 +128,18 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) ? "No tag specified." : $"No selected mod contains the tag \"{_tag}\" locally." : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name.Text))}"; - ImGui.SameLine(); + ImUtf8.SameLineInner(); if (ImUtf8.ButtonEx(label, tooltip, width, _removeMods.Count == 0)) foreach (var (mod, index) in _removeMods) editor.ChangeLocalTag(mod, index, string.Empty); + + if (predefinedTagsEnabled) + { + ImUtf8.SameLineInner(); + tagManager.DrawToggleButton(); + tagManager.DrawListMulti(selector.SelectedPaths.OfType().Select(l => l.Value)); + } + ImGui.Separator(); } diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 7e268e8c..5a3a4b62 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -8,6 +8,8 @@ using OtterGui.Classes; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; @@ -52,6 +54,9 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer jObj.WriteTo(jWriter); } + public bool Enabled + => Count > 0; + public void Save() => _saveService.DelaySave(this, TimeSpan.FromSeconds(5)); @@ -98,9 +103,9 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer } public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, - Mods.Mod mod) + Mod mod) { - DrawToggleButton(); + DrawToggleButtonTopRight(); if (!DrawList(localTags, modTags, editLocal, out var changedTag, out var index)) return; @@ -110,17 +115,22 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer _modManager.DataEditor.ChangeModTag(mod, index, changedTag); } - private void DrawToggleButton() + public void DrawToggleButton() { - ImGui.SameLine(ImGui.GetContentRegionMax().X - - ImGui.GetFrameHeight() - - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), _isListOpen); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Predefined Tags...", false, true)) _isListOpen = !_isListOpen; } + private void DrawToggleButtonTopRight() + { + ImGui.SameLine(ImGui.GetContentRegionMax().X + - ImGui.GetFrameHeight() + - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); + DrawToggleButton(); + } + private bool DrawList(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, out string changedTag, out int changedIndex) { @@ -130,7 +140,7 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer if (!_isListOpen) return false; - ImGui.TextUnformatted("Predefined Tags"); + ImUtf8.Text("Predefined Tags"u8); ImGui.Separator(); var ret = false; @@ -155,6 +165,101 @@ public sealed class PredefinedTagManager : ISavable, IReadOnlyList, ISer return ret; } + private readonly List _selectedMods = []; + private readonly List<(int Index, int DataIndex)> _countedMods = []; + + private void PrepareLists(IEnumerable selection) + { + _selectedMods.Clear(); + _selectedMods.AddRange(selection); + _countedMods.EnsureCapacity(_selectedMods.Count); + while (_countedMods.Count < _selectedMods.Count) + _countedMods.Add((-1, -1)); + } + + public void DrawListMulti(IEnumerable selection) + { + if (!_isListOpen) + return; + + ImUtf8.Text("Predefined Tags"u8); + PrepareLists(selection); + + _enabledColor = ColorId.PredefinedTagAdd.Value(); + _disabledColor = ColorId.PredefinedTagRemove.Value(); + using var color = new ImRaii.Color(); + foreach (var (tag, idx) in _predefinedTags.Keys.WithIndex()) + { + var alreadyContained = 0; + var inModData = 0; + var missing = 0; + + foreach (var (modIndex, mod) in _selectedMods.Index()) + { + var tagIdx = mod.LocalTags.IndexOf(tag); + if (tagIdx >= 0) + { + ++alreadyContained; + _countedMods[modIndex] = (tagIdx, -1); + } + else + { + var dataIdx = mod.ModTags.IndexOf(tag); + if (dataIdx >= 0) + { + ++inModData; + _countedMods[modIndex] = (-1, dataIdx); + } + else + { + ++missing; + _countedMods[modIndex] = (-1, -1); + } + } + } + + using var id = ImRaii.PushId(idx); + var buttonWidth = CalcTextButtonWidth(tag); + // Prevent adding a new tag past the right edge of the popup + if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) + ImGui.NewLine(); + + var (usedColor, disabled, tt) = (missing, alreadyContained) switch + { + (> 0, _) => (_enabledColor, false, + $"Add this tag to {missing} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}"), + (_, > 0) => (_disabledColor, false, + $"Remove this tag from {alreadyContained} mods.{(inModData > 0 ? $" {inModData} mods contain it in their mod tags and are untouched." : string.Empty)}"), + _ => (_disabledColor, true, "This tag is already present in the mod tags of all selected mods."), + }; + color.Push(ImGuiCol.Button, usedColor); + if (ImUtf8.ButtonEx(tag, tt, new Vector2(buttonWidth, 0), disabled)) + { + if (missing > 0) + foreach (var (mod, (localIdx, _)) in _selectedMods.Zip(_countedMods)) + { + if (localIdx >= 0) + continue; + + _modManager.DataEditor.ChangeLocalTag(mod, mod.LocalTags.Count, tag); + } + else + foreach (var (mod, (localIdx, _)) in _selectedMods.Zip(_countedMods)) + { + if (localIdx < 0) + continue; + + _modManager.DataEditor.ChangeLocalTag(mod, localIdx, string.Empty); + } + } + ImGui.SameLine(); + + color.Pop(); + } + + ImGui.NewLine(); + } + private bool DrawColoredButton(string buttonLabel, int index, int tagIdx, bool inOther) { using var id = ImRaii.PushId(index); From 23257f94a4ad4db3b25bed01f9774a3e1512b3f1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Aug 2025 15:41:10 +0200 Subject: [PATCH 052/108] Some cleanup and add option to disable skin material attribute scanning. --- Penumbra/DebugConfiguration.cs | 3 ++- .../Hooks/Resources/ResolvePathHooksBase.cs | 2 +- .../Processing/SkinMtrlPathEarlyProcessing.cs | 21 +++++++++++-------- .../UI/Tabs/Debug/DebugConfigurationDrawer.cs | 15 ++++++------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Penumbra/DebugConfiguration.cs b/Penumbra/DebugConfiguration.cs index 76987df8..3f9e8207 100644 --- a/Penumbra/DebugConfiguration.cs +++ b/Penumbra/DebugConfiguration.cs @@ -2,5 +2,6 @@ namespace Penumbra; public class DebugConfiguration { - public static bool WriteImcBytesToLog = false; + public static bool WriteImcBytesToLog = false; + public static bool UseSkinMaterialProcessing = true; } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index eecb98c5..db39889e 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -162,7 +162,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) { var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex); - if (finalPathBuffer != 0 && finalPathBuffer == pathBuffer) + if (DebugConfiguration.UseSkinMaterialProcessing && finalPathBuffer != nint.Zero && finalPathBuffer == pathBuffer) SkinMtrlPathEarlyProcessing.Process(new Span((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex); return ResolvePath(drawObject, finalPathBuffer); diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs index d35845e1..4487eb7f 100644 --- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -7,7 +7,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing { public static void Process(Span path, CharacterBase* character, uint slotIndex) { - var end = path.IndexOf(".mtrl\0"u8); + var end = path.IndexOf(MaterialExtension()); if (end < 0) return; @@ -23,16 +23,22 @@ public static unsafe class SkinMtrlPathEarlyProcessing if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7) return; - skinSuffix.CopyTo(path[(suffixPos + 1)..]); - ".mtrl\0"u8.CopyTo(path[(suffixPos + 1 + skinSuffix.Length)..]); + ++suffixPos; + skinSuffix.CopyTo(path[suffixPos..]); + suffixPos += skinSuffix.Length; + MaterialExtension().CopyTo(path[suffixPos..]); + return; + + static ReadOnlySpan MaterialExtension() + => ".mtrl\0"u8; } private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex) { - if (character == null) + if (character is null) return null; - if (character->TempSlotData != null) + if (character->TempSlotData is not null) { // TODO ClientStructs-ify var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); @@ -41,10 +47,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing } var model = character->Models[slotIndex]; - if (model == null) - return null; - - return model->ModelResourceHandle; + return model is null ? null : model->ModelResourceHandle; } private static ReadOnlySpan GetSkinSuffix(ModelResourceHandle* handle) diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs index 97761091..087670c1 100644 --- a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -1,15 +1,16 @@ -using OtterGui.Text; - -namespace Penumbra.UI.Tabs.Debug; - +using OtterGui.Text; + +namespace Penumbra.UI.Tabs.Debug; + public static class DebugConfigurationDrawer { public static void Draw() { - using var id = ImUtf8.CollapsingHeaderId("Debug Logging Options"u8); + using var id = ImUtf8.CollapsingHeaderId("Debugging Options"u8); if (!id) return; - ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + ImUtf8.Checkbox("Scan for Skin Material Attributes"u8, ref DebugConfiguration.UseSkinMaterialProcessing); } -} +} From dad01e1af8c52ab3ddc46851b3f885bd5c3e2cf0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Aug 2025 15:24:00 +0200 Subject: [PATCH 053/108] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2cf59c61..15e7c8eb 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2cf59c61494a01fd14aecf925e7dc6325a7374ac +Subproject commit 15e7c8eb41867e6bbd3fe6a8885404df087bc7e7 From b7f326e29c87c31dc1a790a8feb7608a08bacfca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 15:43:55 +0200 Subject: [PATCH 054/108] Fix bug with collection setting and empty collection. --- Penumbra/Collections/CollectionAutoSelector.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs index 68dac914..f6e6bf72 100644 --- a/Penumbra/Collections/CollectionAutoSelector.cs +++ b/Penumbra/Collections/CollectionAutoSelector.cs @@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable return; var collection = _resolver.PlayerCollection(); - Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); - _collections.SetCollection(collection, CollectionType.Current); + if (collection.Identity.Id == Guid.Empty) + { + Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned."); + } + else + { + Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); + _collections.SetCollection(collection, CollectionType.Current); + } } From e3b7f728932da3402f5e479319e459b23d018d74 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 15:44:33 +0200 Subject: [PATCH 055/108] Add initial PCP. --- Penumbra.Api | 2 +- Penumbra/Communication/ModPathChanged.cs | 8 +- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImport.cs | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 3 +- Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Services/PcpService.cs | 259 ++++++++++++++++++ .../UI/AdvancedWindow/ResourceTreeViewer.cs | 56 +++- .../ResourceTreeViewerFactory.cs | 5 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 5 +- Penumbra/UI/Tabs/SettingsTab.cs | 20 +- 11 files changed, 338 insertions(+), 27 deletions(-) create mode 100644 Penumbra/Services/PcpService.cs diff --git a/Penumbra.Api b/Penumbra.Api index c27a0600..2e26d911 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c27a06004138f2ec80ccdb494bb6ddf6d39d2165 +Subproject commit 2e26d9119249e67f03f415f8ebe1dcb7c28d5cf2 diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 1e4f8d36..efe59482 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -3,6 +3,7 @@ using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Services; namespace Penumbra.Communication; @@ -20,11 +21,14 @@ public sealed class ModPathChanged() { public enum Priority { + /// + PcpService = int.MinValue, + /// - ApiMods = int.MinValue, + ApiMods = int.MinValue + 1, /// - ApiModSettings = int.MinValue, + ApiModSettings = int.MinValue + 1, /// EphemeralConfig = -500, diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8c50dad7..e8f1d5ef 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -88,6 +88,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; + public string PcpFolderName { get; set; } = "PCP"; public string QuickMoveFolder1 { get; set; } = string.Empty; public string QuickMoveFolder2 { get; set; } = string.Empty; public string QuickMoveFolder3 { get; set; } = string.Empty; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index fed06573..8e4fea41 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable // Puts out warnings if extension does not correspond to data. private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile) { - if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar") + if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar") return HandleRegularArchive(modPackFile); using var zfs = modPackFile.OpenRead(); diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index fc4fdadc..ffa73b76 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -36,7 +36,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, - string? website) + string? website, params string[] tags) { var mod = new Mod(directory); mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name); @@ -44,6 +44,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; + mod.ModTags = tags; saveService.ImmediateSaveSync(new ModMeta(mod)); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 1bb2a073..3a7bd105 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -32,12 +32,12 @@ public partial class ModCreator( public readonly Configuration Config = config; /// Creates directory and files necessary for a new mod without adding it to the manager. - public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null) + public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null, params string[] tags) { try { var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); - dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty); + dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty, tags); CreateDefaultFiles(newDir); return newDir; } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs new file mode 100644 index 00000000..461045ba --- /dev/null +++ b/Penumbra/Services/PcpService.cs @@ -0,0 +1,259 @@ +using System.Buffers.Text; +using Dalamud.Game.ClientState.Objects.Types; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceTree; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Services; + +public class PcpService : IApiService, IDisposable +{ + public const string Extension = ".pcp"; + + private readonly Configuration _config; + private readonly SaveService _files; + private readonly ResourceTreeFactory _treeFactory; + private readonly ObjectManager _objectManager; + private readonly ActorManager _actors; + private readonly FrameworkManager _framework; + private readonly CollectionResolver _collectionResolver; + private readonly CollectionManager _collections; + private readonly ModCreator _modCreator; + private readonly ModExportManager _modExport; + private readonly CommunicatorService _communicator; + private readonly SHA1 _sha1 = SHA1.Create(); + private readonly ModFileSystem _fileSystem; + + public PcpService(Configuration config, + SaveService files, + ResourceTreeFactory treeFactory, + ObjectManager objectManager, + ActorManager actors, + FrameworkManager framework, + CollectionManager collections, + CollectionResolver collectionResolver, + ModCreator modCreator, + ModExportManager modExport, + CommunicatorService communicator, + ModFileSystem fileSystem) + { + _config = config; + _files = files; + _treeFactory = treeFactory; + _objectManager = objectManager; + _actors = actors; + _framework = framework; + _collectionResolver = collectionResolver; + _collections = collections; + _modCreator = modCreator; + _modExport = modExport; + _communicator = communicator; + _fileSystem = fileSystem; + + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.PcpService); + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) + { + if (type is not ModPathChangeType.Added || newDirectory is null) + return; + + try + { + var file = Path.Combine(newDirectory.FullName, "collection.json"); + if (!File.Exists(file)) + return; + + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var identifier = _actors.FromJson(jObj["Actor"] as JObject); + if (!identifier.IsValid) + return; + + if (jObj["Collection"]?.ToObject() is not { } collectionName) + return; + + var name = $"PCP/{collectionName}"; + if (!_collections.Storage.AddCollection(name, null)) + return; + + var collection = _collections.Storage[^1]; + _collections.Editor.SetModState(collection, mod, true); + + var identifierGroup = _collections.Active.Individuals.GetGroup(identifier); + _collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup); + if (_fileSystem.TryGetValue(mod, out var leaf)) + { + try + { + var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpFolderName); + _fileSystem.Move(leaf, folder); + } + catch + { + // ignored. + } + } + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error reading the collection.json file from {mod.Identifier}:\n{ex}"); + } + } + + public void Dispose() + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + + public async Task<(bool, string)> CreatePcp(ObjectIndex objectIndex, string note = "", CancellationToken cancel = default) + { + try + { + var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() => + { + var (actor, identifier) = CheckActor(objectIndex); + cancel.ThrowIfCancellationRequested(); + unsafe + { + var collection = _collectionResolver.IdentifyCollection((GameObject*)actor.Address, true); + if (!collection.Valid || !collection.ModCollection.HasCache) + throw new Exception($"Actor {identifier} has no mods applying, nothing to do."); + + cancel.ThrowIfCancellationRequested(); + if (_treeFactory.FromCharacter(actor, 0) is not { } tree) + throw new Exception($"Unable to fetch modded resources for {identifier}."); + + return (identifier.CreatePermanent(), tree, collection); + } + }); + cancel.ThrowIfCancellationRequested(); + var time = DateTime.Now; + var modDirectory = CreateMod(identifier, note, time); + await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel); + await CreateCollectionInfo(modDirectory, identifier, note, time, cancel); + var file = ZipUp(modDirectory); + return (true, file); + } + catch (Exception ex) + { + return (false, ex.Message); + } + } + + private static string ZipUp(DirectoryInfo directory) + { + var fileName = directory.FullName + Extension; + ZipFile.CreateFromDirectory(directory.FullName, fileName, CompressionLevel.Optimal, false); + directory.Delete(true); + return fileName; + } + + private static async Task CreateCollectionInfo(DirectoryInfo directory, ActorIdentifier actor, string note, DateTime time, + CancellationToken cancel = default) + { + var jObj = new JObject + { + ["Version"] = 1, + ["Actor"] = actor.ToJson(), + ["Collection"] = note.Length > 0 ? $"{actor.ToName()}: {note}" : actor.ToName(), + ["Time"] = time, + ["Note"] = note, + }; + if (note.Length > 0) + cancel.ThrowIfCancellationRequested(); + var filePath = Path.Combine(directory.FullName, "collection.json"); + await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); + await using var stream = new StreamWriter(file); + await using var json = new JsonTextWriter(stream); + json.Formatting = Formatting.Indented; + await jObj.WriteToAsync(json, cancel); + } + + private DirectoryInfo CreateMod(ActorIdentifier actor, string note, DateTime time) + { + var directory = _modExport.ExportDirectory; + directory.Create(); + var actorName = actor.ToName(); + var authorName = _actors.GetCurrentPlayer().ToName(); + var suffix = note.Length > 0 + ? note + : time.ToString("yyyy-MM-ddTHH\\:mm", CultureInfo.InvariantCulture); + var modName = $"{actorName} - {suffix}"; + var description = $"On-Screen Data for {actorName} as snapshotted on {time}."; + return _modCreator.CreateEmptyMod(directory, modName, description, authorName, "PCP") + ?? throw new Exception($"Unable to create mod {modName} in {directory.FullName}."); + } + + private async Task CreateDefaultMod(DirectoryInfo modDirectory, ModCollection collection, ResourceTree tree, + CancellationToken cancel = default) + { + var subDirectory = modDirectory.CreateSubdirectory("files"); + var subMod = new DefaultSubMod(null!); + foreach (var node in tree.FlatNodes) + { + cancel.ThrowIfCancellationRequested(); + var gamePath = node.GamePath; + var fullPath = node.FullPath; + if (fullPath.IsRooted) + { + var hash = await _sha1.ComputeHashAsync(File.OpenRead(fullPath.FullName), cancel).ConfigureAwait(false); + cancel.ThrowIfCancellationRequested(); + var name = Convert.ToHexString(hash) + fullPath.Extension; + var newFile = Path.Combine(subDirectory.FullName, name); + if (!File.Exists(newFile)) + File.Copy(fullPath.FullName, newFile); + subMod.Files.TryAdd(gamePath, new FullPath(newFile)); + } + else if (gamePath.Path != fullPath.InternalName) + { + subMod.FileSwaps.TryAdd(gamePath, fullPath); + } + } + + cancel.ThrowIfCancellationRequested(); + subMod.Manipulations = new MetaDictionary(collection.MetaCache); + + var saveGroup = new ModSaveGroup(modDirectory, subMod, _config.ReplaceNonAsciiOnImport); + var filePath = _files.FileNames.OptionGroupFile(modDirectory.FullName, -1, string.Empty, _config.ReplaceNonAsciiOnImport); + cancel.ThrowIfCancellationRequested(); + await using var fileStream = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); + await using var writer = new StreamWriter(fileStream); + saveGroup.Save(writer); + } + + private (ICharacter Actor, ActorIdentifier Identifier) CheckActor(ObjectIndex objectIndex) + { + var actor = _objectManager[objectIndex]; + if (!actor.Valid) + throw new Exception($"No Actor at index {objectIndex} found."); + + if (!actor.Identifier(_actors, out var identifier)) + throw new Exception($"Could not create valid identifier for actor at index {objectIndex}."); + + if (!actor.IsCharacter) + throw new Exception($"Actor {identifier} at index {objectIndex} is not a valid character."); + + if (!actor.Model.Valid) + throw new Exception($"Actor {identifier} at index {objectIndex} has no model."); + + if (_objectManager.Objects.CreateObjectReference(actor.Address) is not ICharacter character) + throw new Exception($"Actor {identifier} at index {objectIndex} could not be converted to ICharacter"); + + return (character, identifier); + } +} diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 440baa2f..a2309343 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,15 +1,19 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; using Dalamud.Bindings.ImGui; -using OtterGui.Raii; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility; using OtterGui; +using OtterGui.Classes; +using OtterGui.Extensions; +using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.Services; -using Penumbra.UI.Classes; using Penumbra.String; -using OtterGui.Extensions; +using Penumbra.UI.Classes; +using static System.Net.Mime.MediaTypeNames; namespace Penumbra.UI.AdvancedWindow; @@ -21,12 +25,13 @@ public class ResourceTreeViewer( int actionCapacity, Action onRefresh, Action drawActions, - CommunicatorService communicator) + CommunicatorService communicator, + PcpService pcpService) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly HashSet _unfolded = []; + private readonly HashSet _unfolded = []; private readonly Dictionary _filterCache = []; @@ -34,6 +39,7 @@ public class ResourceTreeViewer( private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; private string _nameFilter = string.Empty; private string _nodeFilter = string.Empty; + private string _note = string.Empty; private Task? _task; @@ -83,7 +89,28 @@ public class ResourceTreeViewer( using var id = ImRaii.PushId(index); - ImGui.TextUnformatted($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); + ImUtf8.TextFrameAligned($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Export Character Pack"u8, + "Note that this recomputes the current data of the actor if it still exists, and does not use the cached data."u8)) + { + pcpService.CreatePcp((ObjectIndex)tree.GameObjectIndex, _note).ContinueWith(t => + { + + var (success, text) = t.Result; + + if (success) + Penumbra.Messager.NotificationMessage($"Created {text}.", NotificationType.Success, false); + else + Penumbra.Messager.NotificationMessage(text, NotificationType.Error, false); + }); + _note = string.Empty; + } + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8); + using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); @@ -263,7 +290,8 @@ public class ResourceTreeViewer( using var group = ImUtf8.Group(); using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) { - ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, + new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); } ImGui.SameLine(); @@ -272,7 +300,8 @@ public class ResourceTreeViewer( } else { - ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, + new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); } if (ImGui.IsItemClicked()) @@ -365,9 +394,10 @@ public class ResourceTreeViewer( private static string GetPathStatusDescription(ResourceNode.PathStatus status) => status switch { - ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.", - ResourceNode.PathStatus.NonExistent => "The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.", - _ => "The actual path to this file is unavailable.", + ResourceNode.PathStatus.External => "The actual path to this file is unavailable, because it is managed by external tools.", + ResourceNode.PathStatus.NonExistent => + "The actual path to this file is unavailable, because it seems to have been moved or deleted since it was loaded.", + _ => "The actual path to this file is unavailable.", }; [Flags] diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 10a4aea2..ac06fe1a 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -9,8 +9,9 @@ public class ResourceTreeViewerFactory( ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, IncognitoService incognito, - CommunicatorService communicator) : IService + CommunicatorService communicator, + PcpService pcpService) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator); + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService); } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 16ff7b41..3f3c82aa 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -126,6 +126,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector + "Mod Packs{.ttmp,.ttmp2,.pmp,.pcp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp,.pcp},Archives{.zip,.7z,.rar},Penumbra Character Packs{.pcp}", (s, f) => { if (!s) return; @@ -445,7 +446,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Draw input for the default folder to sort put newly imported mods into. + private void DrawPcpFolder() + { + var tmp = _config.PcpFolderName; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) + _config.PcpFolderName = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder", + "The folder any penumbra character packs are moved to on import.\nLeave blank to import into Root."); + } + /// Draw all settings pertaining to advanced editing of mods. private void DrawModEditorSettings() @@ -1055,7 +1069,7 @@ public class SettingsTab : ITab, IUiService if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); - ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing())); + ImGui.SetCursorPos(new Vector2(xPos, 5 * ImGui.GetFrameHeightWithSpacing())); CustomGui.DrawKofiPatreonButton(Penumbra.Messager, new Vector2(width, 0)); } From 8043e6fb6be5751deb5a33657638a73542728c35 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 15:49:15 +0200 Subject: [PATCH 056/108] Add option to disable PCP. --- Penumbra/Configuration.cs | 1 + Penumbra/Services/PcpService.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e8f1d5ef..b9a0d9ce 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -68,6 +68,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; + public bool DisablePcpHandling { get; set; } = false; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 461045ba..5f4a844d 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -71,7 +71,7 @@ public class PcpService : IApiService, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { - if (type is not ModPathChangeType.Added || newDirectory is null) + if (type is not ModPathChangeType.Added || _config.DisablePcpHandling || newDirectory is null) return; try diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4bed1ef2..143709f4 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -598,6 +598,9 @@ public class SettingsTab : ITab, IUiService Checkbox("Always Open Import at Default Directory", "Open the import window at the location specified here every time, forgetting your previous path.", _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); + Checkbox("Handle PCP Files", + "When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.", + !_config.DisablePcpHandling, v => _config.DisablePcpHandling = !v); DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); From fb34238530f08918fdc18c63e553ca134f4f8fee Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Aug 2025 13:51:50 +0000 Subject: [PATCH 057/108] [CI] Updating repo.json for testing_1.5.0.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a452dc94..cf4fe6cb 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.6", + "TestingAssemblyVersion": "1.5.0.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.7/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" } From 10894d451a525e504422c4b16a3d3022601e8dfe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 18:08:22 +0200 Subject: [PATCH 058/108] Add Pcp Events. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 25 +++++++++++++++++------- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 ++ Penumbra/Communication/PcpCreation.cs | 20 +++++++++++++++++++ Penumbra/Communication/PcpParsing.cs | 21 ++++++++++++++++++++ Penumbra/Configuration.cs | 1 + Penumbra/Services/CommunicatorService.cs | 8 ++++++++ Penumbra/Services/PcpService.cs | 24 ++++++++++++++++------- 9 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 Penumbra/Communication/PcpCreation.cs create mode 100644 Penumbra/Communication/PcpParsing.cs diff --git a/Penumbra.Api b/Penumbra.Api index 2e26d911..0a970295 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2e26d9119249e67f03f415f8ebe1dcb7c28d5cf2 +Subproject commit 0a970295b2398683b1e49c46fd613541e2486210 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 78c62953..55f1e259 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using OtterGui.Compression; using OtterGui.Services; using Penumbra.Api.Enums; @@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable { switch (type) { - case ModPathChangeType.Deleted when oldDirectory != null: - ModDeleted?.Invoke(oldDirectory.Name); - break; - case ModPathChangeType.Added when newDirectory != null: - ModAdded?.Invoke(newDirectory.Name); - break; + case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break; + case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break; case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null: ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); break; @@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable } public void Dispose() - => _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + { + _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + } public Dictionary GetModList() => _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text); @@ -109,6 +108,18 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public event Action? ModAdded; public event Action? ModMoved; + public event Action? CreatingPcp + { + add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi); + remove => _communicator.PcpCreation.Unsubscribe(value!); + } + + public event Action? ParsingPcp + { + add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi); + remove => _communicator.PcpParsing.Unsubscribe(value!); + } + public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod) diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 7ca41324..9e7eb964 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 10; + public const int FeatureVersion = 11; public void Dispose() { diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 7dcee375..0c80626f 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ModDeleted.Provider(pi, api.Mods), IpcSubscribers.ModAdded.Provider(pi, api.Mods), IpcSubscribers.ModMoved.Provider(pi, api.Mods), + IpcSubscribers.CreatingPcp.Provider(pi, api.Mods), + IpcSubscribers.ParsingPcp.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), diff --git a/Penumbra/Communication/PcpCreation.cs b/Penumbra/Communication/PcpCreation.cs new file mode 100644 index 00000000..cb11b3c3 --- /dev/null +++ b/Penumbra/Communication/PcpCreation.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered when the character.json file for a .pcp file is written. +/// +/// Parameter is the JObject that gets written to file. +/// Parameter is the object index of the game object this is written for. +/// +/// +public sealed class PcpCreation() : EventWrapper(nameof(PcpCreation)) +{ + public enum Priority + { + /// + ModsApi = int.MinValue, + } +} diff --git a/Penumbra/Communication/PcpParsing.cs b/Penumbra/Communication/PcpParsing.cs new file mode 100644 index 00000000..95b78951 --- /dev/null +++ b/Penumbra/Communication/PcpParsing.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Classes; + +namespace Penumbra.Communication; + +/// +/// Triggered when the character.json file for a .pcp file is parsed and applied. +/// +/// Parameter is parsed JObject that contains the data. +/// Parameter is the identifier of the created mod. +/// Parameter is the GUID of the created collection. +/// +/// +public sealed class PcpParsing() : EventWrapper(nameof(PcpParsing)) +{ + public enum Priority + { + /// + ModsApi = int.MinValue, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b9a0d9ce..d9a9f5fe 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -69,6 +69,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool DefaultTemporaryMode { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; public bool DisablePcpHandling { get; set; } = false; + public bool AllowPcpIpc { get; set; } = true; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 5d745419..35f15e9e 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,6 +81,12 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); + /// + public readonly PcpCreation PcpCreation = new(); + + /// + public readonly PcpParsing PcpParsing = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -105,5 +111,7 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); + PcpCreation.Dispose(); + PcpParsing.Dispose(); } } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 5f4a844d..32eca652 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -1,4 +1,3 @@ -using System.Buffers.Text; using Dalamud.Game.ClientState.Objects.Types; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Newtonsoft.Json; @@ -76,9 +75,16 @@ public class PcpService : IApiService, IDisposable try { - var file = Path.Combine(newDirectory.FullName, "collection.json"); + var file = Path.Combine(newDirectory.FullName, "character.json"); if (!File.Exists(file)) - return; + { + // First version had collection.json, changed. + var oldFile = Path.Combine(newDirectory.FullName, "collection.json"); + if (File.Exists(oldFile)) + File.Move(oldFile, file, true); + else + return; + } var text = File.ReadAllText(file); var jObj = JObject.Parse(text); @@ -110,10 +116,12 @@ public class PcpService : IApiService, IDisposable // ignored. } } + if (_config.AllowPcpIpc) + _communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id); } catch (Exception ex) { - Penumbra.Log.Error($"Error reading the collection.json file from {mod.Identifier}:\n{ex}"); + Penumbra.Log.Error($"Error reading the character.json file from {mod.Identifier}:\n{ex}"); } } @@ -145,7 +153,7 @@ public class PcpService : IApiService, IDisposable var time = DateTime.Now; var modDirectory = CreateMod(identifier, note, time); await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel); - await CreateCollectionInfo(modDirectory, identifier, note, time, cancel); + await CreateCollectionInfo(modDirectory, objectIndex, identifier, note, time, cancel); var file = ZipUp(modDirectory); return (true, file); } @@ -163,7 +171,7 @@ public class PcpService : IApiService, IDisposable return fileName; } - private static async Task CreateCollectionInfo(DirectoryInfo directory, ActorIdentifier actor, string note, DateTime time, + private async Task CreateCollectionInfo(DirectoryInfo directory, ObjectIndex index, ActorIdentifier actor, string note, DateTime time, CancellationToken cancel = default) { var jObj = new JObject @@ -176,7 +184,9 @@ public class PcpService : IApiService, IDisposable }; if (note.Length > 0) cancel.ThrowIfCancellationRequested(); - var filePath = Path.Combine(directory.FullName, "collection.json"); + if (_config.AllowPcpIpc) + await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index)); + var filePath = Path.Combine(directory.FullName, "character.json"); await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); await using var stream = new StreamWriter(file); await using var json = new JsonTextWriter(stream); From 0d643840592bac17b67a09dc66f971fed8dc35a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Aug 2025 20:31:40 +0200 Subject: [PATCH 059/108] Add cleanup buttons to PCP, add option to turn off PCP IPC. --- Penumbra/Services/PcpService.cs | 26 +++++++++++++++++++++++++- Penumbra/UI/Tabs/SettingsTab.cs | 21 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 32eca652..73c61cdb 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -38,6 +38,7 @@ public class PcpService : IApiService, IDisposable private readonly CommunicatorService _communicator; private readonly SHA1 _sha1 = SHA1.Create(); private readonly ModFileSystem _fileSystem; + private readonly ModManager _mods; public PcpService(Configuration config, SaveService files, @@ -50,7 +51,8 @@ public class PcpService : IApiService, IDisposable ModCreator modCreator, ModExportManager modExport, CommunicatorService communicator, - ModFileSystem fileSystem) + ModFileSystem fileSystem, + ModManager mods) { _config = config; _files = files; @@ -64,10 +66,27 @@ public class PcpService : IApiService, IDisposable _modExport = modExport; _communicator = communicator; _fileSystem = fileSystem; + _mods = mods; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.PcpService); } + public void CleanPcpMods() + { + var mods = _mods.Where(m => m.ModTags.Contains("PCP")).ToList(); + Penumbra.Log.Information($"[PCPService] Deleting {mods.Count} mods containing the tag PCP."); + foreach (var mod in mods) + _mods.DeleteMod(mod); + } + + public void CleanPcpCollections() + { + var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList(); + Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP."); + foreach (var collection in collections) + _collections.Storage.Delete(collection); + } + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { if (type is not ModPathChangeType.Added || _config.DisablePcpHandling || newDirectory is null) @@ -80,12 +99,14 @@ public class PcpService : IApiService, IDisposable { // First version had collection.json, changed. var oldFile = Path.Combine(newDirectory.FullName, "collection.json"); + Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json."); if (File.Exists(oldFile)) File.Move(oldFile, file, true); else return; } + Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying."); var text = File.ReadAllText(file); var jObj = JObject.Parse(text); var identifier = _actors.FromJson(jObj["Actor"] as JObject); @@ -116,6 +137,7 @@ public class PcpService : IApiService, IDisposable // ignored. } } + if (_config.AllowPcpIpc) _communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id); } @@ -132,6 +154,7 @@ public class PcpService : IApiService, IDisposable { try { + Penumbra.Log.Information($"[PCPService] Creating PCP file for game object {objectIndex.Index}."); var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() => { var (actor, identifier) = CheckActor(objectIndex); @@ -178,6 +201,7 @@ public class PcpService : IApiService, IDisposable { ["Version"] = 1, ["Actor"] = actor.ToJson(), + ["Mod"] = directory.Name, ["Collection"] = note.Length > 0 ? $"{actor.ToName()}: {note}" : actor.ToName(), ["Time"] = time, ["Note"] = note, diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 143709f4..a6d03593 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -52,6 +52,7 @@ public class SettingsTab : ITab, IUiService private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; private readonly AttributeHook _attributeHook; + private readonly PcpService _pcpService; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -64,7 +65,7 @@ public class SettingsTab : ITab, IUiService DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, - AttributeHook attributeHook) + AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; _config = config; @@ -90,6 +91,7 @@ public class SettingsTab : ITab, IUiService _autoSelector = autoSelector; _cleanupService = cleanupService; _attributeHook = attributeHook; + _pcpService = pcpService; } public void DrawHeader() @@ -601,6 +603,23 @@ public class SettingsTab : ITab, IUiService Checkbox("Handle PCP Files", "When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.", !_config.DisablePcpHandling, v => _config.DisablePcpHandling = !v); + + var active = _config.DeleteModModifier.IsActive(); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Delete all PCP Mods"u8, "Deletes all mods tagged with 'PCP' from the mod list."u8, disabled: !active)) + _pcpService.CleanPcpMods(); + if (!active) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, disabled: !active)) + _pcpService.CleanPcpCollections(); + if (!active) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); + + Checkbox("Allow Other Plugins Access to PCP Handling", + "When creating or importing PCP files, other plugins can add and interpret their own data to the character.json file.", + _config.AllowPcpIpc, v => _config.AllowPcpIpc = v); DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); From d302a17f1f2ca2f792acc8f28cf4722f3ded9be6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Aug 2025 18:33:43 +0000 Subject: [PATCH 060/108] [CI] Updating repo.json for testing_1.5.0.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cf4fe6cb..48d5b97f 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.7", + "TestingAssemblyVersion": "1.5.0.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.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" } From 6079103505c3b0b99c26e041f59c26c41b13a543 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 23 Aug 2025 14:46:19 +0200 Subject: [PATCH 061/108] Add collection PCP settings. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/Communication/PcpCreation.cs | 3 +- Penumbra/Configuration.cs | 19 ++++++---- Penumbra/Services/PcpService.cs | 51 ++++++++++++++++----------- Penumbra/UI/Tabs/SettingsTab.cs | 19 +++++++--- 6 files changed, 61 insertions(+), 35 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 0a970295..297941bc 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 0a970295b2398683b1e49c46fd613541e2486210 +Subproject commit 297941bc22300f4a8368f4d0177f62943eb69727 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 55f1e259..1f4f1cf4 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -108,7 +108,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public event Action? ModAdded; public event Action? ModMoved; - public event Action? CreatingPcp + public event Action? CreatingPcp { add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi); remove => _communicator.PcpCreation.Unsubscribe(value!); diff --git a/Penumbra/Communication/PcpCreation.cs b/Penumbra/Communication/PcpCreation.cs index cb11b3c3..ca0cfcf6 100644 --- a/Penumbra/Communication/PcpCreation.cs +++ b/Penumbra/Communication/PcpCreation.cs @@ -8,9 +8,10 @@ namespace Penumbra.Communication; /// /// Parameter is the JObject that gets written to file. /// Parameter is the object index of the game object this is written for. +/// Parameter is the full path to the directory being set up for the PCP creation. /// /// -public sealed class PcpCreation() : EventWrapper(nameof(PcpCreation)) +public sealed class PcpCreation() : EventWrapper(nameof(PcpCreation)) { public enum Priority { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index d9a9f5fe..f9cad217 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; +public record PcpSettings +{ + public bool CreateCollection { get; set; } = true; + public bool AssignCollection { get; set; } = true; + public bool AllowIpc { get; set; } = true; + public bool DisableHandling { get; set; } = false; + public string FolderName { get; set; } = "PCP"; +} + [Serializable] public class Configuration : IPluginConfiguration, ISavable, IService { @@ -68,11 +77,10 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; - public bool DisablePcpHandling { get; set; } = false; - public bool AllowPcpIpc { get; set; } = true; - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; - public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public PcpSettings PcpSettings = new(); + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); @@ -90,7 +98,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; - public string PcpFolderName { get; set; } = "PCP"; public string QuickMoveFolder1 { get; set; } = string.Empty; public string QuickMoveFolder2 { get; set; } = string.Empty; public string QuickMoveFolder3 { get; set; } = string.Empty; diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 73c61cdb..b9d472aa 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -89,7 +89,7 @@ public class PcpService : IApiService, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { - if (type is not ModPathChangeType.Added || _config.DisablePcpHandling || newDirectory is null) + if (type is not ModPathChangeType.Added || _config.PcpSettings.DisableHandling || newDirectory is null) return; try @@ -107,29 +107,37 @@ public class PcpService : IApiService, IDisposable } Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying."); - var text = File.ReadAllText(file); - var jObj = JObject.Parse(text); - var identifier = _actors.FromJson(jObj["Actor"] as JObject); - if (!identifier.IsValid) - return; + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); + var collection = ModCollection.Empty; + // Create collection. + if (_config.PcpSettings.CreateCollection) + { + var identifier = _actors.FromJson(jObj["Actor"] as JObject); + if (identifier.IsValid && jObj["Collection"]?.ToObject() is { } collectionName) + { + var name = $"PCP/{collectionName}"; + if (_collections.Storage.AddCollection(name, null)) + { + collection = _collections.Storage[^1]; + _collections.Editor.SetModState(collection, mod, true); - if (jObj["Collection"]?.ToObject() is not { } collectionName) - return; + // Assign collection. + if (_config.PcpSettings.AssignCollection) + { + var identifierGroup = _collections.Active.Individuals.GetGroup(identifier); + _collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup); + } + } + } + } - var name = $"PCP/{collectionName}"; - if (!_collections.Storage.AddCollection(name, null)) - return; - - var collection = _collections.Storage[^1]; - _collections.Editor.SetModState(collection, mod, true); - - var identifierGroup = _collections.Active.Individuals.GetGroup(identifier); - _collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup); + // Move to folder. if (_fileSystem.TryGetValue(mod, out var leaf)) { try { - var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpFolderName); + var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpSettings.FolderName); _fileSystem.Move(leaf, folder); } catch @@ -138,7 +146,8 @@ public class PcpService : IApiService, IDisposable } } - if (_config.AllowPcpIpc) + // Invoke IPC. + if (_config.PcpSettings.AllowIpc) _communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id); } catch (Exception ex) @@ -208,8 +217,8 @@ public class PcpService : IApiService, IDisposable }; if (note.Length > 0) cancel.ThrowIfCancellationRequested(); - if (_config.AllowPcpIpc) - await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index)); + if (_config.PcpSettings.AllowIpc) + await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index, directory.FullName)); var filePath = Path.Combine(directory.FullName, "character.json"); await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew); await using var stream = new StreamWriter(file); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a6d03593..ded56bb1 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -602,7 +602,7 @@ public class SettingsTab : ITab, IUiService _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); Checkbox("Handle PCP Files", "When encountering specific mods, usually but not necessarily denoted by a .pcp file ending, Penumbra will automatically try to create an associated collection and assign it to a specific character for this mod package. This can turn this behaviour off if unwanted.", - !_config.DisablePcpHandling, v => _config.DisablePcpHandling = !v); + !_config.PcpSettings.DisableHandling, v => _config.PcpSettings.DisableHandling = !v); var active = _config.DeleteModModifier.IsActive(); ImGui.SameLine(); @@ -612,14 +612,23 @@ public class SettingsTab : ITab, IUiService ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); ImGui.SameLine(); - if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, disabled: !active)) + if (ImUtf8.ButtonEx("Delete all PCP Collections"u8, "Deletes all collections whose name starts with 'PCP/' from the collection list."u8, + disabled: !active)) _pcpService.CleanPcpCollections(); if (!active) ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking."); Checkbox("Allow Other Plugins Access to PCP Handling", "When creating or importing PCP files, other plugins can add and interpret their own data to the character.json file.", - _config.AllowPcpIpc, v => _config.AllowPcpIpc = v); + _config.PcpSettings.AllowIpc, v => _config.PcpSettings.AllowIpc = v); + + Checkbox("Create PCP Collections", + "When importing PCP files, create the associated collection.", + _config.PcpSettings.CreateCollection, v => _config.PcpSettings.CreateCollection = v); + + Checkbox("Assign PCP Collections", + "When importing PCP files and creating the associated collection, assign it to the associated character.", + _config.PcpSettings.AssignCollection, v => _config.PcpSettings.AssignCollection = v); DrawDefaultModImportPath(); DrawDefaultModAuthor(); DrawDefaultModImportFolder(); @@ -736,10 +745,10 @@ public class SettingsTab : ITab, IUiService /// Draw input for the default folder to sort put newly imported mods into. private void DrawPcpFolder() { - var tmp = _config.PcpFolderName; + var tmp = _config.PcpSettings.FolderName; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) - _config.PcpFolderName = tmp; + _config.PcpSettings.FolderName = tmp; if (ImGui.IsItemDeactivatedAfterEdit()) _config.Save(); From c8b6325a8733cfdbae82cb90b4eb2d903c6c1ca6 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 24 Aug 2025 08:34:54 +0200 Subject: [PATCH 062/108] Add game integrity message to On-Screen --- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 25 +++++++++++++++++-- .../ResourceTreeViewerFactory.cs | 6 +++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index a2309343..617ba30f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -1,7 +1,9 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; +using Dalamud.Plugin.Services; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; @@ -13,7 +15,6 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.String; using Penumbra.UI.Classes; -using static System.Net.Mime.MediaTypeNames; namespace Penumbra.UI.AdvancedWindow; @@ -26,7 +27,8 @@ public class ResourceTreeViewer( Action onRefresh, Action drawActions, CommunicatorService communicator, - PcpService pcpService) + PcpService pcpService, + IDataManager gameData) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; @@ -45,6 +47,7 @@ public class ResourceTreeViewer( public void Draw() { + DrawModifiedGameFilesWarning(); DrawControls(); _task ??= RefreshCharacterList(); @@ -130,6 +133,24 @@ public class ResourceTreeViewer( } } + private void DrawModifiedGameFilesWarning() + { + if (!gameData.HasModifiedGameDataFiles) + return; + + using var style = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + + ImUtf8.TextWrapped( + "Dalamud is reporting your FFXIV installation has modified game files. Any mods installed through TexTools will produce this message."u8); + ImUtf8.TextWrapped("Penumbra and some other plugins assume your FFXIV installation is unmodified in order to work."u8); + ImUtf8.TextWrapped( + "Data displayed here may be inaccurate because of this, which, in turn, can break functionality relying on it, such as Character Pack exports/imports, or mod synchronization functions provided by other plugins."u8); + ImUtf8.TextWrapped( + "Exit the game, open XIVLauncher, click the arrow next to Log In and select \"repair game files\" to resolve this issue. Afterwards, do not install any mods with TexTools. Your plugin configurations will remain, as will mods enabled in Penumbra."u8); + + ImGui.Separator(); + } + private void DrawControls() { var yOffset = (ChangedItemDrawer.TypeFilterIconSize.Y - ImGui.GetFrameHeight()) / 2f; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index ac06fe1a..43b60716 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Interop.ResourceTree; using Penumbra.Services; @@ -10,8 +11,9 @@ public class ResourceTreeViewerFactory( ChangedItemDrawer changedItemDrawer, IncognitoService incognito, CommunicatorService communicator, - PcpService pcpService) : IService + PcpService pcpService, + IDataManager gameData) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService); + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData); } From 1fca78fa71239882dc7f88df72c31440a8660cb4 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 24 Aug 2025 06:39:38 +0200 Subject: [PATCH 063/108] Add Kdb files to ResourceTree --- Penumbra.GameData | 2 +- .../Processing/SkinMtrlPathEarlyProcessing.cs | 2 +- .../ResolveContext.PathResolution.cs | 28 +++++++++++++++++++ .../Interop/ResourceTree/ResolveContext.cs | 23 ++++++++++++++- Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 14 ++++++---- Penumbra/Interop/Structs/StructExtensions.cs | 9 ++++++ 7 files changed, 73 insertions(+), 9 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 15e7c8eb..73010350 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 15e7c8eb41867e6bbd3fe6a8885404df087bc7e7 +Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4 diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs index 4487eb7f..6be1b959 100644 --- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -40,7 +40,7 @@ public static unsafe class SkinMtrlPathEarlyProcessing if (character->TempSlotData is not null) { - // TODO ClientStructs-ify + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1564) var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); if (handle != null) return handle; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b6d04769..c204f141 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -338,6 +338,34 @@ internal partial record ResolveContext return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } + private Utf8GamePath ResolveKineDriverModulePath(uint partialSkeletonIndex) + { + // Correctness and Safety: + // Resolving a KineDriver module path through the game's code can use EST metadata for human skeletons. + // Additionally, it can dereference null pointers for human equipment skeletons. + return ModelType switch + { + ModelType.Human => ResolveHumanKineDriverModulePath(partialSkeletonIndex), + _ => ResolveKineDriverModulePathNative(partialSkeletonIndex), + }; + } + + private Utf8GamePath ResolveHumanKineDriverModulePath(uint partialSkeletonIndex) + { + var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); + if (set.Id is 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Kdb.Customization(raceCode, slot, set); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveKineDriverModulePathNative(uint partialSkeletonIndex) + { + var path = CharacterBase->ResolveKdbPathAsByteString(partialSkeletonIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) { var animation = ResolveImcData(imc).MaterialAnimationId; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index b2364e33..bbe9b8ce 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -371,7 +371,8 @@ internal unsafe partial record ResolveContext( return node; } - public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) + public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, ResourceHandle* kdbHandle, + uint partialSkeletonIndex) { if (sklb is null || sklb->SkeletonResourceHandle is null) return null; @@ -386,6 +387,8 @@ internal unsafe partial record ResolveContext( node.Children.Add(skpNode); if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode) node.Children.Add(phybNode); + if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode) + node.Children.Add(kdbNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); return node; @@ -427,6 +430,24 @@ internal unsafe partial record ResolveContext( return node; } + private ResourceNode? CreateNodeFromKdb(ResourceHandle* kdbHandle, uint partialSkeletonIndex) + { + if (kdbHandle is null) + return null; + + var path = ResolveKineDriverModulePath(partialSkeletonIndex); + + if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Phyb, 0, kdbHandle, path, false); + if (Global.WithUiData) + node.FallbackName = "KineDriver Module"; + Global.Nodes.Add((path, (nint)kdbHandle), node); + + return node; + } + internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) { var path = gamePath.Path.Split((byte)'/'); diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 3699ae0b..08dee818 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -45,7 +45,9 @@ public class ResourceNode : ICloneable /// Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). public bool Protected - => ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd; + => ForceProtected + || Internal + || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Skp or ResourceType.Phyb or ResourceType.Kdb or ResourceType.Pbd; internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index ddef347d..23fe26b8 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -121,7 +121,7 @@ public class ResourceTree( } } - AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); + AddSkeleton(Nodes, genericContext, model); AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton); AddWeapons(globalContext, model); @@ -178,8 +178,7 @@ public class ResourceTree( } } - AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, - $"Weapon #{weaponIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, "); AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton, $"Weapon #{weaponIndex}, "); @@ -242,8 +241,11 @@ public class ResourceTree( } } + private unsafe void AddSkeleton(List nodes, ResolveContext context, CharacterBase* model, string prefix = "") + => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix); + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, - string prefix = "") + void* kineDriver, string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) @@ -259,7 +261,9 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null; - if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) + // 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.Global.WithUiData) sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 031d24b1..5a29bb6f 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -64,6 +64,15 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); } + public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1561) + var vf80 = (delegate* unmanaged)((nint*)character.VirtualTable)[80]; + var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, + partialSkeletonIndex)); + } + private static unsafe CiByteString ToOwnedByteString(CStringPointer str) => str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty; From f51f8a7bf80f5560c9a88251cad8766a71e17692 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 24 Aug 2025 15:24:50 +0200 Subject: [PATCH 064/108] Try to filter meta entries for relevance. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 182 ++++++++++++++++++ Penumbra/Services/PcpService.cs | 20 +- 2 files changed, 194 insertions(+), 8 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index c2c9e777..8b448ec6 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,7 +1,10 @@ +using System.Collections.Frozen; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; using Penumbra.GameData.Files.AtchStructs; +using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -40,6 +43,165 @@ public class MetaDictionary foreach (var geqp in cache.GlobalEqp.Keys) Add(geqp); } + + public static unsafe Wrapper Filtered(MetaCache cache, Actor actor) + { + if (!actor.IsCharacter) + return new Wrapper(cache); + + var model = actor.Model; + if (!model.IsHuman) + return new Wrapper(cache); + + var headId = model.GetModelId(HumanSlot.Head); + var bodyId = model.GetModelId(HumanSlot.Body); + var equipIdSet = ((IEnumerable) + [ + headId, + bodyId, + model.GetModelId(HumanSlot.Hands), + model.GetModelId(HumanSlot.Legs), + model.GetModelId(HumanSlot.Feet), + ]).ToFrozenSet(); + var earsId = model.GetModelId(HumanSlot.Ears); + var neckId = model.GetModelId(HumanSlot.Neck); + var wristId = model.GetModelId(HumanSlot.Wrists); + var rFingerId = model.GetModelId(HumanSlot.RFinger); + var lFingerId = model.GetModelId(HumanSlot.LFinger); + + var wrapper = new Wrapper(); + // Check for all relevant primary IDs due to slot overlap. + foreach (var (eqp, value) in cache.Eqp) + { + if (eqp.Slot.IsEquipment()) + { + if (equipIdSet.Contains(eqp.SetId)) + wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot)); + } + else + { + switch (eqp.Slot) + { + case EquipSlot.Ears when eqp.SetId == earsId: + case EquipSlot.Neck when eqp.SetId == neckId: + case EquipSlot.Wrists when eqp.SetId == wristId: + case EquipSlot.RFinger when eqp.SetId == rFingerId: + case EquipSlot.LFinger when eqp.SetId == lFingerId: + wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot)); + break; + } + } + } + + // Check also for body IDs due to body occupying head. + foreach (var (gmp, value) in cache.Gmp) + { + if (gmp.SetId == headId || gmp.SetId == bodyId) + wrapper.Gmp.Add(gmp, value.Entry); + } + + // Check for all races due to inheritance and all slots due to overlap. + foreach (var (eqdp, value) in cache.Eqdp) + { + if (eqdp.Slot.IsEquipment()) + { + if (equipIdSet.Contains(eqdp.SetId)) + wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot)); + } + else + { + switch (eqdp.Slot) + { + case EquipSlot.Ears when eqdp.SetId == earsId: + case EquipSlot.Neck when eqdp.SetId == neckId: + case EquipSlot.Wrists when eqdp.SetId == wristId: + case EquipSlot.RFinger when eqdp.SetId == rFingerId: + case EquipSlot.LFinger when eqdp.SetId == lFingerId: + wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot)); + break; + } + } + } + + var genderRace = (GenderRace)model.AsHuman->RaceSexId; + var hairId = model.GetModelId(HumanSlot.Hair); + var faceId = model.GetModelId(HumanSlot.Face); + // We do not need to care for racial inheritance for ESTs. + foreach (var (est, value) in cache.Est) + { + switch (est.Slot) + { + case EstType.Hair when est.SetId == hairId && est.GenderRace == genderRace: + case EstType.Face when est.SetId == faceId && est.GenderRace == genderRace: + case EstType.Body when est.SetId == bodyId && est.GenderRace == genderRace: + case EstType.Head when (est.SetId == headId || est.SetId == bodyId) && est.GenderRace == genderRace: + wrapper.Est.Add(est, value.Entry); + break; + } + } + + foreach (var (geqp, _) in cache.GlobalEqp) + { + switch (geqp.Type) + { + case GlobalEqpType.DoNotHideEarrings when geqp.Condition != earsId: + case GlobalEqpType.DoNotHideNecklace when geqp.Condition != neckId: + case GlobalEqpType.DoNotHideBracelets when geqp.Condition != wristId: + case GlobalEqpType.DoNotHideRingR when geqp.Condition != rFingerId: + case GlobalEqpType.DoNotHideRingL when geqp.Condition != lFingerId: + continue; + default: wrapper.Add(geqp); break; + } + } + + var (_, _, main, off) = model.GetWeapons(actor); + foreach (var (imc, value) in cache.Imc) + { + switch (imc.ObjectType) + { + case ObjectType.Equipment when equipIdSet.Contains(imc.PrimaryId): wrapper.Imc.Add(imc, value.Entry); break; + + case ObjectType.Weapon: + if (imc.PrimaryId == main.Skeleton && imc.SecondaryId == main.Weapon) + wrapper.Imc.Add(imc, value.Entry); + else if (imc.PrimaryId == off.Skeleton && imc.SecondaryId == off.Weapon) + wrapper.Imc.Add(imc, value.Entry); + break; + case ObjectType.Accessory: + switch (imc.EquipSlot) + { + case EquipSlot.Ears when imc.PrimaryId == earsId: + case EquipSlot.Neck when imc.PrimaryId == neckId: + case EquipSlot.Wrists when imc.PrimaryId == wristId: + case EquipSlot.RFinger when imc.PrimaryId == rFingerId: + case EquipSlot.LFinger when imc.PrimaryId == lFingerId: + wrapper.Imc.Add(imc, value.Entry); + break; + } + + break; + } + } + + var subRace = (SubRace)model.AsHuman->Customize[4]; + foreach (var (rsp, value) in cache.Rsp) + { + if (rsp.SubRace == subRace) + wrapper.Rsp.Add(rsp, value.Entry); + } + + // Keep all atch, atr and shp. + wrapper.Atch.EnsureCapacity(cache.Atch.Count); + wrapper.Shp.EnsureCapacity(cache.Shp.Count); + wrapper.Atr.EnsureCapacity(cache.Atr.Count); + foreach (var (atch, value) in cache.Atch) + wrapper.Atch.Add(atch, value.Entry); + foreach (var (shp, value) in cache.Shp) + wrapper.Shp.Add(shp, value.Entry); + foreach (var (atr, value) in cache.Atr) + wrapper.Atr.Add(atr, value.Entry); + return wrapper; + } } private Wrapper? _data; @@ -934,4 +1096,24 @@ public class MetaDictionary _data = new Wrapper(cache); Count = cache.Count; } + + public MetaDictionary(MetaCache? cache, Actor actor) + { + if (cache is null) + return; + + _data = Wrapper.Filtered(cache, actor); + Count = _data.Count + + _data.Eqp.Count + + _data.Eqdp.Count + + _data.Est.Count + + _data.Gmp.Count + + _data.Imc.Count + + _data.Rsp.Count + + _data.Atch.Count + + _data.Atr.Count + + _data.Shp.Count; + if (Count is 0) + _data = null; + } } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index b9d472aa..f75d3b5e 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -107,8 +107,8 @@ public class PcpService : IApiService, IDisposable } Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying."); - var text = File.ReadAllText(file); - var jObj = JObject.Parse(text); + var text = File.ReadAllText(file); + var jObj = JObject.Parse(text); var collection = ModCollection.Empty; // Create collection. if (_config.PcpSettings.CreateCollection) @@ -164,7 +164,7 @@ public class PcpService : IApiService, IDisposable try { Penumbra.Log.Information($"[PCPService] Creating PCP file for game object {objectIndex.Index}."); - var (identifier, tree, collection) = await _framework.Framework.RunOnFrameworkThread(() => + var (identifier, tree, meta) = await _framework.Framework.RunOnFrameworkThread(() => { var (actor, identifier) = CheckActor(objectIndex); cancel.ThrowIfCancellationRequested(); @@ -178,13 +178,14 @@ public class PcpService : IApiService, IDisposable if (_treeFactory.FromCharacter(actor, 0) is not { } tree) throw new Exception($"Unable to fetch modded resources for {identifier}."); - return (identifier.CreatePermanent(), tree, collection); + var meta = new MetaDictionary(collection.ModCollection.MetaCache, actor.Address); + return (identifier.CreatePermanent(), tree, meta); } }); cancel.ThrowIfCancellationRequested(); var time = DateTime.Now; var modDirectory = CreateMod(identifier, note, time); - await CreateDefaultMod(modDirectory, collection.ModCollection, tree, cancel); + await CreateDefaultMod(modDirectory, meta, tree, cancel); await CreateCollectionInfo(modDirectory, objectIndex, identifier, note, time, cancel); var file = ZipUp(modDirectory); return (true, file); @@ -242,11 +243,15 @@ public class PcpService : IApiService, IDisposable ?? throw new Exception($"Unable to create mod {modName} in {directory.FullName}."); } - private async Task CreateDefaultMod(DirectoryInfo modDirectory, ModCollection collection, ResourceTree tree, + private async Task CreateDefaultMod(DirectoryInfo modDirectory, MetaDictionary meta, ResourceTree tree, CancellationToken cancel = default) { var subDirectory = modDirectory.CreateSubdirectory("files"); - var subMod = new DefaultSubMod(null!); + var subMod = new DefaultSubMod(null!) + { + Manipulations = meta, + }; + foreach (var node in tree.FlatNodes) { cancel.ThrowIfCancellationRequested(); @@ -269,7 +274,6 @@ public class PcpService : IApiService, IDisposable } cancel.ThrowIfCancellationRequested(); - subMod.Manipulations = new MetaDictionary(collection.MetaCache); var saveGroup = new ModSaveGroup(modDirectory, subMod, _config.ReplaceNonAsciiOnImport); var filePath = _files.FileNames.OptionGroupFile(modDirectory.FullName, -1, string.Empty, _config.ReplaceNonAsciiOnImport); From 1e07e434985ce55cd47d783d1e6dc7f48e29c7b9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 24 Aug 2025 13:51:43 +0000 Subject: [PATCH 065/108] [CI] Updating repo.json for testing_1.5.0.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 48d5b97f..446932b5 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.8", + "TestingAssemblyVersion": "1.5.0.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.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.0.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a14347f73a39ae0579d721f9b77b05f3e989c8b0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:13:31 +0200 Subject: [PATCH 066/108] Update temporary collection creation. --- Penumbra.Api | 2 +- Penumbra/Api/Api/IdentityChecker.cs | 7 +++++++ Penumbra/Api/Api/TemporaryApi.cs | 12 ++++++++++-- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 4 +++- 4 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 Penumbra/Api/Api/IdentityChecker.cs diff --git a/Penumbra.Api b/Penumbra.Api index 297941bc..af41b178 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 297941bc22300f4a8368f4d0177f62943eb69727 +Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5 diff --git a/Penumbra/Api/Api/IdentityChecker.cs b/Penumbra/Api/Api/IdentityChecker.cs new file mode 100644 index 00000000..e090053e --- /dev/null +++ b/Penumbra/Api/Api/IdentityChecker.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Api.Api; + +public static class IdentityChecker +{ + public static bool Check(string identity) + => true; +} diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index a997ded8..7567acd3 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -20,8 +20,16 @@ public class TemporaryApi( ApiHelpers apiHelpers, ModManager modManager) : IPenumbraApiTemporary, IApiService { - public Guid CreateTemporaryCollection(string name) - => tempCollections.CreateTemporaryCollection(name); + public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name) + { + if (!IdentityChecker.Check(identity)) + return (PenumbraApiEc.InvalidCredentials, Guid.Empty); + + var collection = tempCollections.CreateTemporaryCollection(name); + if (collection == Guid.Empty) + return (PenumbraApiEc.UnknownError, collection); + return (PenumbraApiEc.Success, collection); + } public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId) => tempCollections.RemoveTemporaryCollection(collectionId) diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 64adf256..d46c5728 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -38,6 +38,7 @@ public class TemporaryIpcTester( private string _tempGamePath = "test/game/path.mtrl"; private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; + private string _identity = string.Empty; private PenumbraApiEc _lastTempError; private int _tempActorIndex; private bool _forceOverwrite; @@ -48,6 +49,7 @@ public class TemporaryIpcTester( if (!_) return; + ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128); ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); @@ -73,7 +75,7 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection"); if (ImGui.Button("Create##Collection")) { - LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName); + _lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId); if (_tempGuid == null) { _tempGuid = LastCreatedCollectionId; From bf90725dd2db6b300577fa0c64d309b5277eedee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:13:39 +0200 Subject: [PATCH 067/108] Fix resolvecontext issue. --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index bbe9b8ce..501bbc56 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -440,7 +440,7 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Phyb, 0, kdbHandle, path, false); + var node = CreateNode(ResourceType.Kdb, 0, kdbHandle, path, false); if (Global.WithUiData) node.FallbackName = "KineDriver Module"; Global.Nodes.Add((path, (nint)kdbHandle), node); From 79a4fc5904501fb30dd879ec37d8513c328ea120 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:13:48 +0200 Subject: [PATCH 068/108] Fix wrong logging. --- Penumbra/Services/PcpService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index f75d3b5e..63b8eab3 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -99,9 +99,11 @@ public class PcpService : IApiService, IDisposable { // First version had collection.json, changed. var oldFile = Path.Combine(newDirectory.FullName, "collection.json"); - Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json."); if (File.Exists(oldFile)) + { + Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json."); File.Move(oldFile, file, true); + } else return; } From e16800f21649447cc316fa9ce8c7d88518ad19dd Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 25 Aug 2025 08:16:04 +0000 Subject: [PATCH 069/108] [CI] Updating repo.json for testing_1.5.0.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 446932b5..dea56357 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.9", + "TestingAssemblyVersion": "1.5.0.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/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" } From da47c19aeb30fcc293308652503b5cf1985a390d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:25:05 +0200 Subject: [PATCH 070/108] Woops, increment version. --- Penumbra/Api/Api/PenumbraApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 9e7eb964..7304c9c7 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 11; + public const int FeatureVersion = 12; public void Dispose() { From c0120f81af3a713f861f275ad379a18ed14c0091 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Aug 2025 10:37:38 +0200 Subject: [PATCH 071/108] 1.5.1.0 --- Penumbra/Penumbra.json | 2 +- Penumbra/UI/Changelog.cs | 22 ++++++++++++++++-- repo.json | 48 ++++++++++++++++++++-------------------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index bd9a2479..32032282 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -1,5 +1,5 @@ { - "Author": "Ottermandias, Adam, Wintermute", + "Author": "Ottermandias, Nylfae, Adam, Wintermute", "Name": "Penumbra", "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 4b487104..306dcc79 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -63,10 +63,28 @@ public class PenumbraChangelog : IUiService Add1_3_6_4(Changelog); Add1_4_0_0(Changelog); Add1_5_0_0(Changelog); - } - + Add1_5_1_0(Changelog); + } + #region Changelogs + private static void Add1_5_1_0(Changelog log) + => 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) => log.NextVersion("Version 1.5.0.0") .RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.") diff --git a/repo.json b/repo.json index dea56357..4675bccf 100644 --- a/repo.json +++ b/repo.json @@ -1,26 +1,26 @@ [ - { - "Author": "Ottermandias, Adam, Wintermute", - "Name": "Penumbra", - "Punchline": "Runtime mod loader and manager.", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.10", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 13, - "TestingDalamudApiLevel": 13, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/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" - } + { + "Author": "Ottermandias, Nylfae, Adam, Wintermute", + "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", + "Description": "Runtime mod loader and manager.", + "InternalName": "Penumbra", + "AssemblyVersion": "1.5.0.6", + "TestingAssemblyVersion": "1.5.0.10", + "RepoUrl": "https://github.com/xivdev/Penumbra", + "ApplicableVersion": "any", + "DalamudApiLevel": 13, + "TestingDalamudApiLevel": 13, + "IsHide": "False", + "IsTestingExclusive": "False", + "DownloadCount": 0, + "LastUpdate": 0, + "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/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" + } ] From 71e24c13c7915e4741fe20fa86cc6dbebf1d2355 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 25 Aug 2025 08:39:42 +0000 Subject: [PATCH 072/108] [CI] Updating repo.json for 1.5.1.0 --- repo.json | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/repo.json b/repo.json index 4675bccf..e9a52799 100644 --- a/repo.json +++ b/repo.json @@ -1,26 +1,26 @@ [ - { - "Author": "Ottermandias, Nylfae, Adam, Wintermute", - "Name": "Penumbra", - "Punchline": "Runtime mod loader and manager.", - "Description": "Runtime mod loader and manager.", - "InternalName": "Penumbra", - "AssemblyVersion": "1.5.0.6", - "TestingAssemblyVersion": "1.5.0.10", - "RepoUrl": "https://github.com/xivdev/Penumbra", - "ApplicableVersion": "any", - "DalamudApiLevel": 13, - "TestingDalamudApiLevel": 13, - "IsHide": "False", - "IsTestingExclusive": "False", - "DownloadCount": 0, - "LastUpdate": 0, - "LoadPriority": 69420, - "LoadRequiredState": 2, - "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.0.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.0.10/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" - } + { + "Author": "Ottermandias, Nylfae, Adam, Wintermute", + "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", + "Description": "Runtime mod loader and manager.", + "InternalName": "Penumbra", + "AssemblyVersion": "1.5.1.0", + "TestingAssemblyVersion": "1.5.1.0", + "RepoUrl": "https://github.com/xivdev/Penumbra", + "ApplicableVersion": "any", + "DalamudApiLevel": 13, + "TestingDalamudApiLevel": 13, + "IsHide": "False", + "IsTestingExclusive": "False", + "DownloadCount": 0, + "LastUpdate": 0, + "LoadPriority": 69420, + "LoadRequiredState": 2, + "LoadSync": true, + "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" + } ] From a04a5a071c99585f4d4bd749fc6b4f8b9d4dce99 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Aug 2025 18:51:57 +0200 Subject: [PATCH 073/108] Add warning in file redirections if extension doesn't match. --- Penumbra.Api | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index af41b178..953dd227 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5 +Subproject commit 953dd227afda6b3943b0b88cc965d8aee8a879b5 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 87d7487b..63c99b8a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -287,6 +287,17 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.IconFont); 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) @@ -319,6 +330,17 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.IconFont); 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() From f7cf5503bbd4c31b59c081f91b966afbc291b1f3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Aug 2025 18:52:06 +0200 Subject: [PATCH 074/108] Fix deleting PCP collections. --- Penumbra/Services/PcpService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index 63b8eab3..bdf1adc5 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -84,7 +84,7 @@ public class PcpService : IApiService, IDisposable var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList(); Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP."); foreach (var collection in collections) - _collections.Storage.Delete(collection); + _collections.Storage.RemoveCollection(collection); } private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) From 912020cc3f9a08324bb2515b0a35f22b720051cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Aug 2025 16:36:42 +0200 Subject: [PATCH 075/108] Update for staging and wrong tooltip. --- Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs | 5 ++--- Penumbra/Services/PcpService.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs index 6be1b959..bd066d83 100644 --- a/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs +++ b/Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs @@ -38,10 +38,9 @@ public static unsafe class SkinMtrlPathEarlyProcessing if (character is null) return null; - if (character->TempSlotData is not null) + if (character->PerSlotStagingArea is not null) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1564) - var handle = *(ModelResourceHandle**)((nint)character->TempSlotData + 0xE0 * slotIndex + 0x8); + var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle; if (handle != null) return handle; } diff --git a/Penumbra/Services/PcpService.cs b/Penumbra/Services/PcpService.cs index bdf1adc5..17646564 100644 --- a/Penumbra/Services/PcpService.cs +++ b/Penumbra/Services/PcpService.cs @@ -82,7 +82,7 @@ public class PcpService : IApiService, IDisposable public void CleanPcpCollections() { var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList(); - Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} mods containing the tag PCP."); + Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} collections starting with PCP/."); foreach (var collection in collections) _collections.Storage.RemoveCollection(collection); } From 8c25ef4b47486df7b79c63d66c78fcf7710f2112 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 16:53:12 +0200 Subject: [PATCH 076/108] Make the save button ResourceTreeViewer baseline --- .../ModEditWindow.QuickImport.cs | 62 +---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 104 ++++++++++++++---- .../ResourceTreeViewerFactory.cs | 11 +- 4 files changed, 95 insertions(+), 84 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 72350857..f55ae576 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -17,7 +17,6 @@ public partial class ModEditWindow private readonly FileDialogService _fileDialog; private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; - private readonly Dictionary _quickImportWritables = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); private HashSet GetPlayerResourcesOfType(ResourceType type) @@ -56,52 +55,11 @@ public partial class ModEditWindow private void OnQuickImportRefresh() { - _quickImportWritables.Clear(); _quickImportActions.Clear(); } - private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) + private void DrawQuickImportActions(ResourceNode resourceNode, IWritable? writable, 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(); if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport)) { @@ -121,24 +79,6 @@ 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 const string FallbackOptionName = "the current option"; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 952d8489..5a0fb849 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -667,7 +667,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; - _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; if (IsOpen && selection.Mod != null) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 617ba30f..00003451 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -4,16 +4,20 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; +using Lumina.Data; using OtterGui; using OtterGui.Classes; +using OtterGui.Compression; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.String; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -25,17 +29,20 @@ public class ResourceTreeViewer( IncognitoService incognito, int actionCapacity, Action onRefresh, - Action drawActions, + Action drawActions, CommunicatorService communicator, PcpService pcpService, - IDataManager gameData) + IDataManager gameData, + FileDialogService fileDialog, + FileCompactor compactor) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly HashSet _unfolded = []; - private readonly Dictionary _filterCache = []; + private readonly Dictionary _filterCache = []; + private readonly Dictionary _writableCache = []; private TreeCategory _categoryFilter = AllCategories; private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; @@ -115,7 +122,7 @@ public class ResourceTreeViewer( ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8); - using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, + using var table = ImRaii.Table("##ResourceTree", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; @@ -123,9 +130,8 @@ public class ResourceTreeViewer( ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - if (actionCapacity > 0) - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, - (actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight()); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, + actionCapacity * 3 * ImGuiHelpers.GlobalScale + (actionCapacity + 1) * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); @@ -211,6 +217,7 @@ public class ResourceTreeViewer( finally { _filterCache.Clear(); + _writableCache.Clear(); _unfolded.Clear(); onRefresh(); } @@ -221,7 +228,6 @@ public class ResourceTreeViewer( { var debugMode = config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { @@ -291,7 +297,7 @@ public class ResourceTreeViewer( 0 => "(none)", 1 => resourceNode.GamePath.ToString(), _ => "(multiple)", - }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); if (hasGamePaths) { var allPaths = string.Join('\n', resourceNode.PossibleGamePaths); @@ -312,7 +318,7 @@ public class ResourceTreeViewer( using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) { ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); } ImGui.SameLine(); @@ -322,7 +328,7 @@ public class ResourceTreeViewer( else { ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); } if (ImGui.IsItemClicked()) @@ -336,20 +342,17 @@ public class ResourceTreeViewer( else { ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); ImGuiUtil.HoverTooltip( $"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); - if (actionCapacity > 0) - { - ImGui.TableNextColumn(); - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); - drawActions(resourceNode, new Vector2(frameHeight)); - } + ImGui.TableNextColumn(); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); + DrawActions(resourceNode, new Vector2(frameHeight)); if (unfolded) DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); @@ -402,6 +405,51 @@ public class ResourceTreeViewer( || node.FullPath.InternalName.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 GetPathStatusLabel(ResourceNode.PathStatus status) @@ -465,4 +513,22 @@ public class ResourceTreeViewer( Visible = 1, 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; + } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 43b60716..6518ae67 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,5 +1,7 @@ using Dalamud.Plugin.Services; +using OtterGui.Compression; using OtterGui.Services; +using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Services; @@ -12,8 +14,11 @@ public class ResourceTreeViewerFactory( IncognitoService incognito, CommunicatorService communicator, PcpService pcpService, - IDataManager gameData) : IService + IDataManager gameData, + FileDialogService fileDialog, + FileCompactor compactor) : IService { - public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData); + public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData, + fileDialog, compactor); } From b3379a97105d37f685dd0686d89d0bf27c1c0807 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 16:55:20 +0200 Subject: [PATCH 077/108] Stop redacting external paths --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 00003451..cb765fcf 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -37,7 +37,7 @@ public class ResourceTreeViewer( FileCompactor compactor) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = - ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; + ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly HashSet _unfolded = []; From f3ec4b2e081a4cb477f7c85189ac1525586f97c7 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 19:19:07 +0200 Subject: [PATCH 078/108] Only display the file name and last dir for externals --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index cb765fcf..ae450bec 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -325,6 +325,18 @@ public class ResourceTreeViewer( ImGui.SetCursorPosX(textPos); 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 { ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, From 5503bb32e059ed1438ebb139c5da6306e870f3b2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 04:13:56 +0200 Subject: [PATCH 079/108] CloudApi testing in Debug tab --- Penumbra/Interop/CloudApi.cs | 29 ++++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 39 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Penumbra/Interop/CloudApi.cs diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs new file mode 100644 index 00000000..9ec29fa5 --- /dev/null +++ b/Penumbra/Interop/CloudApi.cs @@ -0,0 +1,29 @@ +namespace Penumbra.Interop; + +public static unsafe partial class CloudApi +{ + private const int CfSyncRootInfoBasic = 0; + + public static bool IsCloudSynced(string path) + { + var buffer = stackalloc long[1]; + var hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out var length); + Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT {hr}"); + if (hr < 0) + return false; + + if (length != sizeof(long)) + { + Penumbra.Log.Warning($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); + return false; + } + + Penumbra.Log.Information($"{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); +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index d41dd25a..05f77e29 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; @@ -41,6 +42,7 @@ using Penumbra.GameData.Data; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; @@ -206,6 +208,7 @@ public class DebugTab : Window, ITab, IUiService _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); + DrawCloudApi(); DrawDebugTabIpc(); } @@ -1199,6 +1202,42 @@ 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}"); + } + } + + /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { From d59be1e660e26adce11664ffdbef5631e2511aeb Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 05:25:37 +0200 Subject: [PATCH 080/108] Refine IsCloudSynced --- Penumbra/Interop/CloudApi.cs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs index 9ec29fa5..603d4c9f 100644 --- a/Penumbra/Interop/CloudApi.cs +++ b/Penumbra/Interop/CloudApi.cs @@ -4,21 +4,39 @@ public static unsafe partial class CloudApi { private const int CfSyncRootInfoBasic = 0; + /// Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. + /// Can be expensive. Callers should cache the result when relevant. public static bool IsCloudSynced(string path) { - var buffer = stackalloc long[1]; - var hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out var length); - Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT {hr}"); + 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.Warning($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); + Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); return false; } - Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); return true; } From 2cf60b78cd73f01b6207325a2359663b39745079 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 06:42:45 +0200 Subject: [PATCH 081/108] Reject and warn about cloud-synced base directories --- Penumbra/Mods/Manager/ModManager.cs | 4 ++++ Penumbra/Penumbra.cs | 13 ++++++++----- Penumbra/UI/Tabs/SettingsTab.cs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 32dac049..77385bbd 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,5 +1,6 @@ using OtterGui.Services; using Penumbra.Communication; +using Penumbra.Interop; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Services; @@ -303,6 +304,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService if (!firstTime && _config.ModDirectory != BasePath.FullName) 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) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b22d049d..f036adc7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,6 +23,7 @@ using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData; using Penumbra.GameData.Data; +using Penumbra.Interop; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; @@ -211,10 +212,11 @@ public class Penumbra : IDalamudPlugin public string GatherSupportInformation() { - var sb = new StringBuilder(10240); - var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); - var hdrEnabler = _services.GetService(); - var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; + var sb = new StringBuilder(10240); + var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); + var cloudSynced = exists && CloudApi.IsCloudSynced(_config.ModDirectory); + var hdrEnabler = _services.GetService(); + var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n"); sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); @@ -223,7 +225,8 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n"); if (Dalamud.Utility.Util.IsWine()) sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n"); - sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); + sb.Append( + $"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ded56bb1..308cc471 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,6 +14,7 @@ using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Interop; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; @@ -59,6 +60,9 @@ public class SettingsTab : ITab, IUiService private readonly TagButtons _sharedTags = new(); + private string _lastCloudSyncTestedPath = string.Empty; + private bool _lastCloudSyncTestResult = false; + public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, @@ -208,6 +212,15 @@ public class SettingsTab : ITab, IUiService if (IsSubPathOf(gameDir, newName)) 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 ? ($"Press Enter or Click Here to Save (Current Directory: {old})", true) : ($"Click Here to Save (Current Directory: {old})", true); From ad1659caf637c6919f4cb3f03e918496cf5fc23b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 11:29:58 +0200 Subject: [PATCH 082/108] Update libraries. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/OtterGui b/OtterGui index 4a9b71a9..f3544447 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89 +Subproject commit f354444776591ae423e2d8374aae346308d81424 diff --git a/Penumbra.Api b/Penumbra.Api index 953dd227..dd141317 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 953dd227afda6b3943b0b88cc965d8aee8a879b5 +Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index abcb8e3d..1b1f0a28 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.GameData b/Penumbra.GameData index 73010350..3450df1f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 73010350338ecd7b98ad85d127bed08d7d8718d4 +Subproject commit 3450df1f377543a226ded705e3db9e77ed2a0510 diff --git a/Penumbra.String b/Penumbra.String index 878acce4..c8611a0c 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd +Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 3159b736..fa45ffbf 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas From 4e788f7c2bfb5bf04f8e22d6ac56b489ff6ad942 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 11:51:59 +0200 Subject: [PATCH 083/108] Update sig. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3450df1f..27893a85 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3450df1f377543a226ded705e3db9e77ed2a0510 +Subproject commit 27893a85adb57a301dd93fd2c7d318bfd4c12a0f From f5f6dd3246202a186ca205afec4d4673219a673a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 16:12:01 +0200 Subject: [PATCH 084/108] Handle some TODOs. --- Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs | 3 +-- .../Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 4 ++-- Penumbra/Interop/Structs/StructExtensions.cs | 5 +---- Penumbra/Mods/Editor/ModMerger.cs | 1 - 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index e0eb7ec5..cdd82b95 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -63,8 +63,7 @@ public sealed unsafe class LoadTimelineResources : FastHook**)timeline)[0][29](timeline); + var idx = timeline->GetOwningGameObjectIndex(); if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b9c21556..dd708e51 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -434,7 +434,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private static MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) { // TODO ClientStructs-ify - var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; + var unkPointer = unkPayload->ModelResourceHandle.*(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; var materialIndex = *(ushort*)(unkPointer + 8); var material = unkPayload->Params->Model->Materials[materialIndex]; if (material == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 23fe26b8..345dd0fd 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -242,10 +242,10 @@ public class ResourceTree( } private unsafe void AddSkeleton(List nodes, ResolveContext context, CharacterBase* model, string prefix = "") - => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, *(void**)((nint)model + 0x160), prefix); + => AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix); private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, - void* kineDriver, string prefix = "") + BoneKineDriverModule* kineDriver, string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 5a29bb6f..7349f6cc 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -66,11 +66,8 @@ internal static class StructExtensions public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1561) - var vf80 = (delegate* unmanaged)((nint*)character.VirtualTable)[80]; var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; - return ToOwnedByteString(vf80((CharacterBase*)Unsafe.AsPointer(ref character), pathBuffer, CharacterBase.PathBufferSize, - partialSkeletonIndex)); + return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex)); } private static unsafe CiByteString ToOwnedByteString(CStringPointer str) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index bb84173a..eb270e13 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -372,7 +372,6 @@ public class ModMerger : IDisposable, IService } else { - // TODO DataContainer <> Option. var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group!.Name, option!.Name); From 5a6e06df3ba6a7ed056199b03f540ac567a52be9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Sep 2025 16:22:02 +0200 Subject: [PATCH 085/108] git is stupid --- .../Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index dd708e51..b9c21556 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -434,7 +434,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private static MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) { // TODO ClientStructs-ify - var unkPointer = unkPayload->ModelResourceHandle.*(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; + var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; var materialIndex = *(ushort*)(unkPointer + 8); var material = unkPayload->Params->Model->Materials[materialIndex]; if (material == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 345dd0fd..1ebfe53d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -261,8 +261,7 @@ public class ResourceTree( for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null; - // TODO ClientStructs-ify (aers/FFXIVClientStructs#1562) - var kdbHandle = kineDriver != null ? *(ResourceHandle**)((nint)kineDriver + 0x20 + 0x18 * i) : null; + var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null; if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) From 6348c4a639811786d2302ac021914dcd89a65a2b Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 2 Sep 2025 14:25:55 +0000 Subject: [PATCH 086/108] [CI] Updating repo.json for 1.5.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e9a52799..9ff227b6 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.0", - "TestingAssemblyVersion": "1.5.1.0", + "AssemblyVersion": "1.5.1.2", + "TestingAssemblyVersion": "1.5.1.2", "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.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", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c3b00ff42613270e3a8452dcafebaa795b9c226b Mon Sep 17 00:00:00 2001 From: Stoia <23234609+StoiaCode@users.noreply.github.com> Date: Sat, 6 Sep 2025 14:22:18 +0200 Subject: [PATCH 087/108] Integrate FileWatcher HEAVY WIP --- Penumbra/Configuration.cs | 2 + Penumbra/Penumbra.cs | 2 + Penumbra/Services/FileWatcher.cs | 136 +++++++++++++++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 47 ++++++++++- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 Penumbra/Services/FileWatcher.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f9cad217..500d5d57 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -53,6 +53,7 @@ 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; @@ -76,6 +77,7 @@ 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 EnableCustomShapes { get; set; } = true; public PcpSettings PcpSettings = new(); public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f036adc7..0f5703a3 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -44,6 +44,7 @@ public class Penumbra : IDalamudPlugin private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly ModManager _modManager; + private readonly FileWatcher _fileWatcher; private readonly CollectionManager _collectionManager; private readonly Configuration _config; private readonly CharacterUtility _characterUtility; @@ -81,6 +82,7 @@ public class Penumbra : IDalamudPlugin _residentResources = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _modManager = _services.GetService(); + _fileWatcher = _services.GetService(); _collectionManager = _services.GetService(); _tempCollections = _services.GetService(); _redrawService = _services.GetService(); diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs new file mode 100644 index 00000000..8a2f9402 --- /dev/null +++ b/Penumbra/Services/FileWatcher.cs @@ -0,0 +1,136 @@ +using System.Threading.Channels; +using OtterGui.Services; +using Penumbra.Mods.Manager; + +namespace Penumbra.Services; +public class FileWatcher : IDisposable, IService +{ + private readonly FileSystemWatcher _fsw; + private readonly Channel _queue; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _consumer; + private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); + private readonly ModImportManager _modImportManager; + private readonly Configuration _config; + private readonly bool _enabled; + + public FileWatcher(ModImportManager modImportManager, Configuration config) + { + _config = config; + _modImportManager = modImportManager; + _enabled = config.EnableDirectoryWatch; + + if (!_enabled) return; + + _queue = Channel.CreateBounded(new BoundedChannelOptions(256) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.DropOldest + }); + + _fsw = new FileSystemWatcher(_config.WatchDirectory) + { + 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; + + _consumer = Task.Factory.StartNew( + () => ConsumerLoopAsync(_cts.Token), + _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); + + _fsw.EnableRaisingEvents = true; + } + + private void OnPath(object? sender, FileSystemEventArgs e) + { + // Cheap de-dupe: only queue once per filename until processed + if (!_enabled || !_pending.TryAdd(e.FullPath, 0)) return; + _ = _queue.Writer.TryWrite(e.FullPath); + } + + private async Task ConsumerLoopAsync(CancellationToken token) + { + if (!_enabled) return; + var reader = _queue.Reader; + while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) + { + while (reader.TryRead(out var path)) + { + try + { + await ProcessOneAsync(path, token).ConfigureAwait(false); + } + catch (OperationCanceledException) { Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); } + catch (Exception ex) + { + Penumbra.Log.Debug($"[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 (int 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) + { + _modImportManager.AddUnpack(path); + 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 UpdateDirectory(string newPath) + { + if (!_enabled || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + + _fsw.EnableRaisingEvents = false; + _fsw.Path = newPath; + _fsw.EnableRaisingEvents = true; + } + + public void Dispose() + { + if (!_enabled) return; + _fsw.EnableRaisingEvents = false; + _cts.Cancel(); + _fsw.Dispose(); + _queue.Writer.TryComplete(); + try { _consumer.Wait(TimeSpan.FromSeconds(5)); } catch { /* swallow */ } + _cts.Dispose(); + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 308cc471..c84214f3 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -37,6 +37,7 @@ 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; @@ -65,7 +66,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, HttpApi httpApi, + CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, @@ -82,6 +83,7 @@ public class SettingsTab : ITab, IUiService _characterUtility = characterUtility; _residentResources = residentResources; _modExportManager = modExportManager; + _fileWatcher = fileWatcher; _httpApi = httpApi; _dalamudSubstitutionProvider = dalamudSubstitutionProvider; _compactor = compactor; @@ -647,6 +649,10 @@ public class SettingsTab : ITab, IUiService DrawDefaultModImportFolder(); DrawPcpFolder(); DrawDefaultModExportPath(); + Checkbox("Enable Automatic Import of Mods from Directory", + "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to automatically import these mods.", + _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); + DrawFileWatcherPath(); } @@ -726,6 +732,45 @@ public class SettingsTab : ITab, IUiService + "Keep this empty to use the root directory."); } + private string _tempWatchDirectory = string.Empty; + /// Draw input for the Automatic Mod import path. + private void DrawFileWatcherPath() + { + var tmp = _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.IsItemDeactivatedAfterEdit()) + _fileWatcher.UpdateDirectory(_tempWatchDirectory); + + 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); + _config.WatchDirectory = s; + _config.Save(); + } + }, startDir, false); + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker("Automatic Import Director", + "Choose the Directory the File Watcher listens to."); + } + /// Draw input for the default name to input as author into newly generated mods. private void DrawDefaultModAuthor() { From 97c8d82b338be04c513df4d15f1ef72a6fbbed4c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Sep 2025 10:45:28 +0200 Subject: [PATCH 088/108] Prevent default-named collection from being renamed and always put it at the top of the selector. --- Penumbra/UI/CollectionTab/CollectionPanel.cs | 44 ++++++++++--------- .../UI/CollectionTab/CollectionSelector.cs | 3 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 26fa2b14..e41ceade 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -11,6 +11,7 @@ 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; @@ -222,26 +223,31 @@ public sealed class CollectionPanel( ImGui.EndGroup(); ImGui.SameLine(); ImGui.BeginGroup(); - 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) + var width = ImGui.GetContentRegionAvail().X; + using (ImRaii.Disabled(_collections.DefaultNamed == collection)) { - collection.Identity.Name = _newName; - saveService.QueueSave(new ModCollectionSave(mods, collection)); - selector.RestoreCollections(); - _newName = null; - } - else if (ImGui.IsItemDeactivated()) - { - _newName = null; + 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; + } } + 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))) @@ -375,9 +381,7 @@ 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; diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index e54f994e..79254090 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -116,7 +116,8 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl public void RestoreCollections() { Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Identity.Name)) + Items.Add(_storage.DefaultNamed); + foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed)) Items.Add(c); SetFilterDirty(); SetCurrent(_active.Current); From e9f67a009be51377226186d61b10340683f5d3f3 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 19 Sep 2025 03:50:28 +0200 Subject: [PATCH 089/108] Lift "shaders known" restriction for saving materials --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index e15d1c90..2c7c889e 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -216,7 +216,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable } public bool Valid - => _shadersKnown && Mtrl.Valid; + => Mtrl.Valid; // FIXME This should be _shadersKnown && Mtrl.Valid but the algorithm for _shadersKnown is flawed as of 7.2. public byte[] Write() { From a59689ebfe043b14d4c87f09bad3baddd10bea78 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Sep 2025 13:00:12 +0200 Subject: [PATCH 090/108] CS API update and add http API routes. --- Penumbra/Api/HttpApi.cs | 58 +++++++++++++++++++--- Penumbra/Interop/Services/RedrawService.cs | 4 +- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 8f8b44f4..dca9426a 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -5,6 +5,7 @@ using EmbedIO.WebApi; using OtterGui.Services; using Penumbra.Api.Api; using Penumbra.Api.Enums; +using Penumbra.Mods.Settings; namespace Penumbra.Api; @@ -13,13 +14,15 @@ public class HttpApi : IDisposable, IApiService private partial class Controller : WebApiController { // @formatter:off - [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.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(); // @formatter:on } @@ -65,6 +68,12 @@ 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."); @@ -116,6 +125,7 @@ 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().ConfigureAwait(false); @@ -124,6 +134,30 @@ public class HttpApi : IDisposable, IApiService api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name); } + public async partial Task SetModSettings() + { + var data = await HttpContext.GetRequestDataAsync().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.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() @@ -151,5 +185,15 @@ public class HttpApi : IDisposable, IApiService : this(string.Empty, RedrawType.Redraw, -1) { } } + + private record SetModSettingsData( + Guid? CollectionId, + string ModPath, + string ModName, + bool? Inherit, + bool? State, + ModPriority? Priority, + Dictionary>? Settings) + { } } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 08e9ddf5..2d741277 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable return; - foreach (ref var f in currentTerritory->Furniture) + foreach (ref var f in currentTerritory->FurnitureManager.FurnitureMemory) { - var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null; + var gameObject = f.Index >= 0 ? currentTerritory->FurnitureManager.ObjectManager.ObjectArray.Objects[f.Index].Value : null; if (gameObject == null) continue; From a0c3e820b0e9be6080f83d10447971bdaba5681d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 27 Sep 2025 11:02:39 +0000 Subject: [PATCH 091/108] [CI] Updating repo.json for testing_1.5.1.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9ff227b6..bac039b8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.2", + "TestingAssemblyVersion": "1.5.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c6b596169c0f970a7e4ee7bdf21f89347de8c0d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Sep 2025 14:01:21 +0200 Subject: [PATCH 092/108] Add default constructor. --- Penumbra/Api/HttpApi.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index dca9426a..995a6cd7 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -194,6 +194,10 @@ public class HttpApi : IDisposable, IApiService bool? State, ModPriority? Priority, Dictionary>? Settings) - { } + { + public SetModSettingsData() + : this(null, string.Empty, string.Empty, null, null, null, null) + {} + } } } From eb53f04c6b2b88806227981fdbc8c53f193e0ada Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 27 Sep 2025 12:03:35 +0000 Subject: [PATCH 093/108] [CI] Updating repo.json for testing_1.5.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index bac039b8..f404b8af 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.3", + "TestingAssemblyVersion": "1.5.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 699745413e224f2b55e9eb7bf014e13c821408c9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Sep 2025 12:40:52 +0200 Subject: [PATCH 094/108] Make priority an int. --- Penumbra/Api/HttpApi.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 995a6cd7..79348a88 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -151,7 +151,7 @@ public class HttpApi : IDisposable, IApiService 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.Value); + 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); } @@ -192,7 +192,7 @@ public class HttpApi : IDisposable, IApiService string ModName, bool? Inherit, bool? State, - ModPriority? Priority, + int? Priority, Dictionary>? Settings) { public SetModSettingsData() From 23c0506cb875f8613513f4169630eeb6549cc6ef Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 28 Sep 2025 10:43:01 +0000 Subject: [PATCH 095/108] [CI] Updating repo.json for testing_1.5.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f404b8af..d6a7dd4c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.4", + "TestingAssemblyVersion": "1.5.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0881dfde8a26ebcea56bab0c9c5eadeca8884039 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Oct 2025 12:27:35 +0200 Subject: [PATCH 096/108] Update signatures. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 27893a85..7e7d510a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 27893a85adb57a301dd93fd2c7d318bfd4c12a0f +Subproject commit 7e7d510a2ce78e2af78312a8b2215c23bf43a56f From 049baa4fe49c0386532dd096663fc4368fd9dcf8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Oct 2025 12:42:54 +0200 Subject: [PATCH 097/108] Again. --- Penumbra.GameData | 2 +- Penumbra/Penumbra.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 7e7d510a..3baace73 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 7e7d510a2ce78e2af78312a8b2215c23bf43a56f +Subproject commit 3baace73c828271dcb71a8156e3e7b91e1dd12ae diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f036adc7..d433a0fb 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,7 +21,6 @@ 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; From 300e0e6d8484f44c00a9320b48e068b10ea2ab1c Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 7 Oct 2025 10:45:04 +0000 Subject: [PATCH 098/108] [CI] Updating repo.json for 1.5.1.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index d6a7dd4c..2a31b75e 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.2", - "TestingAssemblyVersion": "1.5.1.5", + "AssemblyVersion": "1.5.1.6", + "TestingAssemblyVersion": "1.5.1.6", "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.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ebbe957c95d44d2b1569c4e22b3a7cd672246385 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Oct 2025 20:09:52 +0200 Subject: [PATCH 099/108] Remove login screen log spam. --- Penumbra/Interop/PathResolving/CollectionResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 10795e6d..136393d4 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -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.Verbose( + Penumbra.Log.Excessive( $"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) { From 7ed81a982365fa99164a2ab5d8cdb6801987c0d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Oct 2025 17:53:02 +0200 Subject: [PATCH 100/108] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index f3544447..9af1e5fc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f354444776591ae423e2d8374aae346308d81424 +Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 From f05cb52da2a77dc8b6bcd5cad3dd4b32d97febb3 Mon Sep 17 00:00:00 2001 From: Stoia <23234609+StoiaCode@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:20:44 +0200 Subject: [PATCH 101/108] Add Option to notify instead of auto install. And General Fixes --- Penumbra/Configuration.cs | 1 + Penumbra/Services/FileWatcher.cs | 42 +++++++++++++++++++++-------- Penumbra/Services/MessageService.cs | 32 ++++++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 11 +++++--- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 500d5d57..e337997b 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -78,6 +78,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService 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; diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index 8a2f9402..e7172f58 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,5 +1,6 @@ using System.Threading.Channels; using OtterGui.Services; +using Penumbra.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; @@ -11,16 +12,16 @@ public class FileWatcher : IDisposable, IService private readonly Task _consumer; private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); private readonly ModImportManager _modImportManager; + private readonly MessageService _messageService; private readonly Configuration _config; - private readonly bool _enabled; - public FileWatcher(ModImportManager modImportManager, Configuration config) + public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) { - _config = config; _modImportManager = modImportManager; - _enabled = config.EnableDirectoryWatch; + _messageService = messageService; + _config = config; - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; _queue = Channel.CreateBounded(new BoundedChannelOptions(256) { @@ -55,13 +56,13 @@ public class FileWatcher : IDisposable, IService private void OnPath(object? sender, FileSystemEventArgs e) { // Cheap de-dupe: only queue once per filename until processed - if (!_enabled || !_pending.TryAdd(e.FullPath, 0)) return; + if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) return; _ = _queue.Writer.TryWrite(e.FullPath); } private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; var reader = _queue.Reader; while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) { @@ -101,8 +102,27 @@ public class FileWatcher : IDisposable, IService var len = fi.Length; if (len > 0 && len == lastLen) { - _modImportManager.AddUnpack(path); - return; + if (_config.EnableAutomaticModImport) + { + _modImportManager.AddUnpack(path); + return; + } + else + { + var invoked = false; + Action installRequest = args => + { + if (invoked) return; + invoked = true; + _modImportManager.AddUnpack(path); + }; + + _messageService.PrintModFoundInfo( + Path.GetFileNameWithoutExtension(path), + installRequest); + + return; + } } lastLen = len; @@ -116,7 +136,7 @@ public class FileWatcher : IDisposable, IService public void UpdateDirectory(string newPath) { - if (!_enabled || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; _fsw.EnableRaisingEvents = false; _fsw.Path = newPath; @@ -125,7 +145,7 @@ public class FileWatcher : IDisposable, IService public void Dispose() { - if (!_enabled) return; + if (!_config.EnableDirectoryWatch) return; _fsw.EnableRaisingEvents = false; _cts.Cancel(); _fsw.Dispose(); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 70ccf47b..6c13fc38 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -1,19 +1,44 @@ +using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; +using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; +using static OtterGui.Classes.MessageService; using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; +public class InstallNotification(string message, Action installRequest) : IMessage +{ + private readonly Action _installRequest = installRequest; + private bool _invoked = false; + + public string Message { get; } = message; + + public NotificationType NotificationType => NotificationType.Info; + + public uint NotificationDuration => 10000; + + public void OnNotificationActions(INotificationDrawArgs args) + { + if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) + { + _installRequest(true); + _invoked = true; + } + } +} + public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { @@ -55,4 +80,11 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", NotificationType.Warning, 10000)); } + + public void PrintModFoundInfo(string fileName, Action installRequest) + { + AddMessage( + new InstallNotification($"A new mod has been found: {fileName}", installRequest) + ); + } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c84214f3..217b6788 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -53,6 +53,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; + private readonly MessageService _messageService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; @@ -69,7 +70,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MessageService messageService, AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; @@ -96,6 +97,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; + _messageService = messageService; _attributeHook = attributeHook; _pcpService = pcpService; } @@ -649,9 +651,12 @@ public class SettingsTab : ITab, IUiService DrawDefaultModImportFolder(); DrawPcpFolder(); DrawDefaultModExportPath(); - Checkbox("Enable Automatic Import of Mods from Directory", - "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to automatically import these mods.", + Checkbox("Enable Directory Watcher", + "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to open a Popup to import these mods.", _config.EnableDirectoryWatch, v => _config.EnableDirectoryWatch = v); + Checkbox("Enable Fully Automatic Import", + "Uses the File Watcher in order to not just open a Popup, but fully automatically import new mods.", + _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); DrawFileWatcherPath(); } From cbedc878b94ceda8cc91105d5b2456b76bda2fdb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Oct 2025 21:56:16 +0200 Subject: [PATCH 102/108] Slight cleanup and autoformat. --- Penumbra/Configuration.cs | 2 +- Penumbra/Penumbra.cs | 2 - Penumbra/Services/FileWatcher.cs | 91 +++++++++++++++++++---------- Penumbra/Services/MessageService.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 8 +-- 5 files changed, 66 insertions(+), 40 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e337997b..2991230e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -53,7 +53,7 @@ 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 string WatchDirectory { get; set; } = string.Empty; public bool? UseCrashHandler { get; set; } = null; public bool OpenWindowAtStart { get; set; } = false; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ed2c585..d433a0fb 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -43,7 +43,6 @@ public class Penumbra : IDalamudPlugin private readonly TempModManager _tempMods; private readonly TempCollectionManager _tempCollections; private readonly ModManager _modManager; - private readonly FileWatcher _fileWatcher; private readonly CollectionManager _collectionManager; private readonly Configuration _config; private readonly CharacterUtility _characterUtility; @@ -81,7 +80,6 @@ public class Penumbra : IDalamudPlugin _residentResources = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _modManager = _services.GetService(); - _fileWatcher = _services.GetService(); _collectionManager = _services.GetService(); _tempCollections = _services.GetService(); _redrawService = _services.GetService(); diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index e7172f58..141825f5 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,40 +1,41 @@ using System.Threading.Channels; using OtterGui.Services; -using Penumbra.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; + public class FileWatcher : IDisposable, IService { - private readonly FileSystemWatcher _fsw; - private readonly Channel _queue; - private readonly CancellationTokenSource _cts = new(); - private readonly Task _consumer; - private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); - private readonly ModImportManager _modImportManager; - private readonly MessageService _messageService; - private readonly Configuration _config; + private readonly FileSystemWatcher _fsw; + private readonly Channel _queue; + private readonly CancellationTokenSource _cts = new(); + private readonly Task _consumer; + private readonly ConcurrentDictionary _pending = new(StringComparer.OrdinalIgnoreCase); + private readonly ModImportManager _modImportManager; + private readonly MessageService _messageService; + private readonly Configuration _config; public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) { _modImportManager = modImportManager; - _messageService = messageService; - _config = config; + _messageService = messageService; + _config = config; - if (!_config.EnableDirectoryWatch) return; + if (!_config.EnableDirectoryWatch) + return; _queue = Channel.CreateBounded(new BoundedChannelOptions(256) { SingleReader = true, SingleWriter = false, - FullMode = BoundedChannelFullMode.DropOldest + FullMode = BoundedChannelFullMode.DropOldest, }); _fsw = new FileSystemWatcher(_config.WatchDirectory) { IncludeSubdirectories = false, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, - InternalBufferSize = 32 * 1024 + NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, + InternalBufferSize = 32 * 1024, }; // Only wake us for the exact patterns we care about @@ -56,13 +57,17 @@ public class FileWatcher : IDisposable, IService private void OnPath(object? sender, FileSystemEventArgs e) { // Cheap de-dupe: only queue once per filename until processed - if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) return; + if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) + return; + _ = _queue.Writer.TryWrite(e.FullPath); } private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_config.EnableDirectoryWatch) return; + if (!_config.EnableDirectoryWatch) + return; + var reader = _queue.Reader; while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) { @@ -72,7 +77,10 @@ public class FileWatcher : IDisposable, IService { await ProcessOneAsync(path, token).ConfigureAwait(false); } - catch (OperationCanceledException) { Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); } + catch (OperationCanceledException) + { + Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); + } catch (Exception ex) { Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}"); @@ -90,15 +98,19 @@ public class FileWatcher : IDisposable, IService // 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; + long lastLen = -1; - for (int i = 0; i < maxTries && !token.IsCancellationRequested; i++) + for (var i = 0; i < maxTries && !token.IsCancellationRequested; i++) { - if (!File.Exists(path)) { await Task.Delay(100, token); continue; } + if (!File.Exists(path)) + { + await Task.Delay(100, token); + continue; + } try { - var fi = new FileInfo(path); + var fi = new FileInfo(path); var len = fi.Length; if (len > 0 && len == lastLen) { @@ -112,7 +124,9 @@ public class FileWatcher : IDisposable, IService var invoked = false; Action installRequest = args => { - if (invoked) return; + if (invoked) + return; + invoked = true; _modImportManager.AddUnpack(path); }; @@ -122,13 +136,19 @@ public class FileWatcher : IDisposable, IService installRequest); return; - } + } } lastLen = len; } - catch (IOException) { Penumbra.Log.Debug($"[FileWatcher] File is still being written to."); } - catch (UnauthorizedAccessException) { Penumbra.Log.Debug($"[FileWatcher] File is locked."); } + 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); } @@ -136,21 +156,32 @@ public class FileWatcher : IDisposable, IService public void UpdateDirectory(string newPath) { - if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) return; + if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) + return; _fsw.EnableRaisingEvents = false; - _fsw.Path = newPath; + _fsw.Path = newPath; _fsw.EnableRaisingEvents = true; } public void Dispose() { - if (!_config.EnableDirectoryWatch) return; + if (!_config.EnableDirectoryWatch) + return; + _fsw.EnableRaisingEvents = false; _cts.Cancel(); _fsw.Dispose(); _queue.Writer.TryComplete(); - try { _consumer.Wait(TimeSpan.FromSeconds(5)); } catch { /* swallow */ } + try + { + _consumer.Wait(TimeSpan.FromSeconds(5)); + } + catch + { + /* swallow */ + } + _cts.Dispose(); } } diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 6c13fc38..3dc6a90c 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -20,7 +20,6 @@ namespace Penumbra.Services; public class InstallNotification(string message, Action installRequest) : IMessage { - private readonly Action _installRequest = installRequest; private bool _invoked = false; public string Message { get; } = message; @@ -33,7 +32,7 @@ public class InstallNotification(string message, Action installRequest) : { if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) { - _installRequest(true); + installRequest(true); _invoked = true; } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 217b6788..46f4d38f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -53,7 +53,6 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; - private readonly MessageService _messageService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; @@ -70,7 +69,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MessageService messageService, + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, AttributeHook attributeHook, PcpService pcpService) { _pluginInterface = pluginInterface; @@ -97,7 +96,6 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; - _messageService = messageService; _attributeHook = attributeHook; _pcpService = pcpService; } @@ -652,10 +650,10 @@ public class SettingsTab : ITab, IUiService DrawPcpFolder(); DrawDefaultModExportPath(); Checkbox("Enable Directory Watcher", - "Enables a File Watcher that automatically listens for Mod files that enter, causing Penumbra to open a Popup to import these mods.", + "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, v => _config.EnableDirectoryWatch = v); Checkbox("Enable Fully Automatic Import", - "Uses the File Watcher in order to not just open a Popup, but fully automatically import new mods.", + "Uses the File Watcher in order to skip the query popup and automatically import any new mods.", _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); DrawFileWatcherPath(); } From 5bf901d0c45f7c0384480387cab03eb626d25899 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Oct 2025 17:30:29 +0200 Subject: [PATCH 103/108] Update actorobjectmanager when setting cutscene index. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Interop/PathResolving/CutsceneService.cs | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 9af1e5fc..a63f6735 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 +Subproject commit a63f6735cf4bed4f7502a022a10378607082b770 diff --git a/Penumbra.Api b/Penumbra.Api index dd141317..c23ee05c 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa +Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 diff --git a/Penumbra.GameData b/Penumbra.GameData index 3baace73..283d51f6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3baace73c828271dcb71a8156e3e7b91e1dd12ae +Subproject commit 283d51f6f6c7721a810548d95ba83eef2484e17e diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 6be19c46..97e64f84 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable return false; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; + _objects.InvokeRequiredUpdates(); return true; } From 912c183fc6e05e58920552ff902078f4accbbde0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Oct 2025 23:45:20 +0200 Subject: [PATCH 104/108] Improve file watcher. --- Penumbra.GameData | 2 +- Penumbra/Services/FileWatcher.cs | 200 +++++++++++++---------- Penumbra/Services/InstallNotification.cs | 39 +++++ Penumbra/Services/MessageService.cs | 31 ---- Penumbra/UI/Tabs/SettingsTab.cs | 26 +-- 5 files changed, 165 insertions(+), 133 deletions(-) create mode 100644 Penumbra/Services/InstallNotification.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 283d51f6..d889f9ef 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 283d51f6f6c7721a810548d95ba83eef2484e17e +Subproject commit d889f9ef918514a46049725052d378b441915b00 diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index 141825f5..1d572f05 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -1,37 +1,69 @@ -using System.Threading.Channels; -using OtterGui.Services; +using OtterGui.Services; using Penumbra.Mods.Manager; namespace Penumbra.Services; public class FileWatcher : IDisposable, IService { - private readonly FileSystemWatcher _fsw; - private readonly Channel _queue; - private readonly CancellationTokenSource _cts = new(); - private readonly Task _consumer; + // TODO: use ConcurrentSet when it supports comparers in Luna. private readonly ConcurrentDictionary _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) + if (_config.EnableDirectoryWatch) + { + SetupFileWatcher(_config.WatchDirectory); + SetupConsumerTask(); + } + } + + public void Toggle(bool value) + { + if (_config.EnableDirectoryWatch == value) return; - _queue = Channel.CreateBounded(new BoundedChannelOptions(256) + _config.EnableDirectoryWatch = value; + _config.Save(); + if (value) { - SingleReader = true, - SingleWriter = false, - FullMode = BoundedChannelFullMode.DropOldest, - }); + SetupFileWatcher(_config.WatchDirectory); + SetupConsumerTask(); + } + else + { + EndFileWatcher(); + EndConsumerTask(); + } + } - _fsw = new FileSystemWatcher(_config.WatchDirectory) + 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, @@ -46,49 +78,81 @@ public class FileWatcher : IDisposable, IService _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(); + } - _fsw.EnableRaisingEvents = true; + 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) - { - // Cheap de-dupe: only queue once per filename until processed - if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0)) - return; - - _ = _queue.Writer.TryWrite(e.FullPath); - } + => _pending.TryAdd(e.FullPath, 0); private async Task ConsumerLoopAsync(CancellationToken token) { - if (!_config.EnableDirectoryWatch) - return; - - var reader = _queue.Reader; - while (await reader.WaitToReadAsync(token).ConfigureAwait(false)) + while (true) { - while (reader.TryRead(out var path)) + var (path, _) = _pending.FirstOrDefault(); + if (path is null || _pausedConsumer) { - try - { - await ProcessOneAsync(path, token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Penumbra.Log.Debug($"[FileWatcher] Canceled via Token."); - } - catch (Exception ex) - { - Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}"); - } - finally - { - _pending.TryRemove(path, out _); - } + 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 _); } } } @@ -115,28 +179,10 @@ public class FileWatcher : IDisposable, IService if (len > 0 && len == lastLen) { if (_config.EnableAutomaticModImport) - { _modImportManager.AddUnpack(path); - return; - } else - { - var invoked = false; - Action installRequest = args => - { - if (invoked) - return; - - invoked = true; - _modImportManager.AddUnpack(path); - }; - - _messageService.PrintModFoundInfo( - Path.GetFileNameWithoutExtension(path), - installRequest); - - return; - } + _messageService.AddMessage(new InstallNotification(_modImportManager, path), false); + return; } lastLen = len; @@ -154,34 +200,10 @@ public class FileWatcher : IDisposable, IService } } - public void UpdateDirectory(string newPath) - { - if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath)) - return; - - _fsw.EnableRaisingEvents = false; - _fsw.Path = newPath; - _fsw.EnableRaisingEvents = true; - } public void Dispose() { - if (!_config.EnableDirectoryWatch) - return; - - _fsw.EnableRaisingEvents = false; - _cts.Cancel(); - _fsw.Dispose(); - _queue.Writer.TryComplete(); - try - { - _consumer.Wait(TimeSpan.FromSeconds(5)); - } - catch - { - /* swallow */ - } - - _cts.Dispose(); + EndConsumerTask(); + EndFileWatcher(); } } diff --git a/Penumbra/Services/InstallNotification.cs b/Penumbra/Services/InstallNotification.cs new file mode 100644 index 00000000..e3956076 --- /dev/null +++ b/Penumbra/Services/InstallNotification.cs @@ -0,0 +1,39 @@ +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(); + } +} diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 3dc6a90c..70ccf47b 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -1,43 +1,19 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; -using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; -using static OtterGui.Classes.MessageService; using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; -public class InstallNotification(string message, Action installRequest) : IMessage -{ - private bool _invoked = false; - - public string Message { get; } = message; - - public NotificationType NotificationType => NotificationType.Info; - - public uint NotificationDuration => 10000; - - public void OnNotificationActions(INotificationDrawArgs args) - { - if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked)) - { - installRequest(true); - _invoked = true; - } - } -} - public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { @@ -79,11 +55,4 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", NotificationType.Warning, 10000)); } - - public void PrintModFoundInfo(string fileName, Action installRequest) - { - AddMessage( - new InstallNotification($"A new mod has been found: {fileName}", installRequest) - ); - } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 46f4d38f..86c01cb2 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -66,7 +66,8 @@ 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, + FileWatcher fileWatcher, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, @@ -651,7 +652,7 @@ public class SettingsTab : ITab, IUiService 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, v => _config.EnableDirectoryWatch = v); + _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); @@ -735,19 +736,24 @@ public class SettingsTab : ITab, IUiService + "Keep this empty to use the root directory."); } - private string _tempWatchDirectory = string.Empty; + private string? _tempWatchDirectory; + /// Draw input for the Automatic Mod import path. private void DrawFileWatcherPath() { - var tmp = _config.WatchDirectory; - var spacing = new Vector2(UiHelpers.ScaleX3); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + 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.IsItemDeactivatedAfterEdit()) - _fileWatcher.UpdateDirectory(_tempWatchDirectory); + 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, @@ -761,11 +767,7 @@ public class SettingsTab : ITab, IUiService _fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) => { if (b) - { _fileWatcher.UpdateDirectory(s); - _config.WatchDirectory = s; - _config.Save(); - } }, startDir, false); } From c4b6e4e00bd4a52b1b5be5059effccae58c8befb Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 23 Oct 2025 21:50:20 +0000 Subject: [PATCH 105/108] [CI] Updating repo.json for testing_1.5.1.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2a31b75e..34405eb6 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.6", - "TestingAssemblyVersion": "1.5.1.6", + "TestingAssemblyVersion": "1.5.1.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ce54aa5d2559abc8552edfe0b270e61c450226c4 Mon Sep 17 00:00:00 2001 From: Karou Date: Sun, 2 Nov 2025 17:58:20 -0500 Subject: [PATCH 106/108] Added IPC call to allow for redrawing only members of specified collections --- Penumbra.Api | 2 +- Penumbra/Api/Api/RedrawApi.cs | 29 ++++++++++++++++--- Penumbra/Api/IpcProviders.cs | 1 + .../Api/IpcTester/CollectionsIpcTester.cs | 4 +++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index c23ee05c..874a3773 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 +Subproject commit 874a3773bc4f637de1ef1fa8756b4debe3d8f68b diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index ec4de892..4cbb9f29 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -2,11 +2,14 @@ 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) : IPenumbraApiRedraw, IApiService +namespace Penumbra.Api.Api; + +public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService { public void RedrawObject(int gameObjectIndex, RedrawType setting) { @@ -28,9 +31,27 @@ public class RedrawApi(RedrawService redrawService, IFramework framework) : IPen 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) + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(actor.ObjectIndex, setting)); + } + } + }); + } + public event GameObjectRedrawnDelegate? GameObjectRedrawn { add => redrawService.GameObjectRedrawn += value; remove => redrawService.GameObjectRedrawn -= value; } -} +} diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 0c80626f..5f04540f 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -88,6 +88,7 @@ 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), diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index c06bdeb4..f033b7c3 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -121,6 +121,10 @@ 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() From 5dd74297c623430ea63ad7a01531ba9b58e75eb7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Nov 2025 22:10:17 +0000 Subject: [PATCH 107/108] [CI] Updating repo.json for 1.5.1.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 34405eb6..7ddffd7c 100644 --- a/repo.json +++ b/repo.json @@ -5,8 +5,8 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.5.1.6", - "TestingAssemblyVersion": "1.5.1.7", + "AssemblyVersion": "1.5.1.8", + "TestingAssemblyVersion": "1.5.1.8", "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.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", + "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", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From ccb5b01290c717b0581ce5c782e9c7554ff27357 Mon Sep 17 00:00:00 2001 From: Karou Date: Sat, 29 Nov 2025 12:14:10 -0500 Subject: [PATCH 108/108] Api version bump and remove redundant framework thread call --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/RedrawApi.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 874a3773..3d6cee1a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 874a3773bc4f637de1ef1fa8756b4debe3d8f68b +Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 7304c9c7..c4026c72 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -17,7 +17,7 @@ public class PenumbraApi( UiApi ui) : IDisposable, IApiService, IPenumbraApi { public const int BreakingVersion = 5; - public const int FeatureVersion = 12; + public const int FeatureVersion = 13; public void Dispose() { diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index 4cbb9f29..08f1f9df 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -43,7 +43,7 @@ public class RedrawApi(RedrawService redrawService, IFramework framework, Collec helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection); if (collection == modCollection) { - framework.RunOnFrameworkThread(() => redrawService.RedrawObject(actor.ObjectIndex, setting)); + redrawService.RedrawObject(actor.ObjectIndex, setting); } } });