From 78a3ff177f8304c7ed9f0bc59b6f15985c6929c1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Mar 2024 13:46:45 +0000 Subject: [PATCH 001/865] [CI] Updating repo.json for testing_1.0.2.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 44ada6e8..2af4a645 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.3", + "TestingAssemblyVersion": "1.0.2.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4175a582b8ece7d088a27c6fd95cd2be5c2e54ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Mar 2024 15:15:41 +0100 Subject: [PATCH 002/865] Add IPC Providers because I'm still a fucking moron. --- Penumbra/Api/PenumbraIpcProviders.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index d478b675..78887156 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -30,7 +30,9 @@ public class PenumbraIpcProviders : IDisposable internal readonly EventProvider ModDirectoryChanged; // UI + internal readonly EventProvider PreSettingsTabBarDraw; internal readonly EventProvider PreSettingsDraw; + internal readonly EventProvider PostEnabledDraw; internal readonly EventProvider PostSettingsDraw; internal readonly EventProvider ChangedItemTooltip; internal readonly EventProvider ChangedItemClick; @@ -130,8 +132,8 @@ public class PenumbraIpcProviders : IDisposable FuncProvider>> GetPlayerResourcesOfType; - internal readonly FuncProvider GetGameObjectResourceTrees; - internal readonly FuncProvider> GetPlayerResourceTrees; + internal readonly FuncProvider GetGameObjectResourceTrees; + internal readonly FuncProvider> GetPlayerResourceTrees; public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) @@ -153,7 +155,11 @@ public class PenumbraIpcProviders : IDisposable ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a); // UI - PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); + PreSettingsTabBarDraw = + Ipc.PreSettingsTabBarDraw.Provider(pi, a => Api.PreSettingsTabBarDraw += a, a => Api.PreSettingsTabBarDraw -= a); + PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); + PostEnabledDraw = + Ipc.PostEnabledDraw.Provider(pi, a => Api.PostEnabledDraw += a, a => Api.PostEnabledDraw -= a); PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a); ChangedItemTooltip = Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip); @@ -278,7 +284,9 @@ public class PenumbraIpcProviders : IDisposable ModDirectoryChanged.Dispose(); // UI + PreSettingsTabBarDraw.Dispose(); PreSettingsDraw.Dispose(); + PostEnabledDraw.Dispose(); PostSettingsDraw.Dispose(); ChangedItemTooltip.Dispose(); ChangedItemClick.Dispose(); From 12532dee2810c9f770a467a2f61c0c007226e373 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Mar 2024 14:17:47 +0000 Subject: [PATCH 003/865] [CI] Updating repo.json for testing_1.0.2.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2af4a645..5a51afa2 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.4", + "TestingAssemblyVersion": "1.0.2.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From efdd5a824b16fb9e5a423062b644cea038a9275f Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 26 Mar 2024 02:29:07 +0100 Subject: [PATCH 004/865] Make human.pbd moddable --- Penumbra/Collections/Cache/CollectionCache.cs | 31 ++++-- .../Interop/ResourceLoading/ResourceLoader.cs | 28 ++++++ .../ResourceLoading/ResourceService.cs | 9 +- .../Interop/ResourceTree/ResolveContext.cs | 9 ++ Penumbra/Interop/ResourceTree/ResourceTree.cs | 18 +++- .../Interop/SafeHandles/SafeResourceHandle.cs | 10 +- Penumbra/Interop/Services/CharacterUtility.cs | 8 ++ .../Services/PreBoneDeformerReplacer.cs | 95 +++++++++++++++++++ .../Interop/Structs/CharacterUtilityData.cs | 4 + Penumbra/Penumbra.cs | 1 + Penumbra/Services/StaticServiceManager.cs | 1 + 11 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 Penumbra/Interop/Services/PreBoneDeformerReplacer.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 9a0e525b..c2c215aa 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -8,6 +8,9 @@ using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; +using Penumbra.Interop.SafeHandles; +using System.IO; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Collections.Cache; @@ -20,13 +23,14 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// public class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, object?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly ConcurrentDictionary LoadedResources = new(); + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -136,6 +140,13 @@ public class CollectionCache : IDisposable public void RemoveMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); + /// Invalidates caches subsequently to a resolved file being modified. + private void InvalidateResolvedFile(Utf8GamePath path) + { + if (LoadedResources.Remove(path, out var handle)) + handle.Dispose(); + } + /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { @@ -148,17 +159,20 @@ public class CollectionCache : IDisposable if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, Mod.ForcedFiles); } else { + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); } } else if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); } } @@ -181,6 +195,7 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.Remove(path, out var mp)) { + InvalidateResolvedFile(path); if (mp.Mod != mod) Penumbra.Log.Warning( $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); @@ -295,6 +310,7 @@ public class CollectionCache : IDisposable if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { ModData.AddPath(mod, path); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -309,6 +325,7 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, path); + InvalidateResolvedFile(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); } } diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 8ccdfa80..1b467a8c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; @@ -39,6 +40,33 @@ public unsafe class ResourceLoader : IDisposable return ret; } + /// Load a resource for a given path and a specific collection. + public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + { + _resolvedData = resolveData; + var ret = _resources.GetSafeResource(category, type, path); + _resolvedData = ResolveData.Invalid; + return ret; + } + + public SafeResourceHandle LoadCacheableSafeResource(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData resolveData) + { + var cache = resolveData.ModCollection._cache; + if (cache == null) + return LoadResolvedSafeResource(category, type, path.Path, resolveData); + + if (cache.LoadedResources.TryGetValue(path, out var cached)) + return cached.Clone(); + + var ret = LoadResolvedSafeResource(category, type, path.Path, resolveData); + + cached = ret.Clone(); + if (!cache.LoadedResources.TryAdd(path, cached)) + cached.Dispose(); + + return ret; + } + /// The function to use to resolve a given path. public Func ResolvePath = null!; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 6fb2e560..e3338e6c 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -4,10 +4,12 @@ using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.GameData; +using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; +using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.ResourceLoading; @@ -25,11 +27,11 @@ public unsafe class ResourceService : IDisposable _getResourceAsyncHook.Enable(); _resourceHandleDestructorHook.Enable(); _incRefHook = interop.HookFromAddress( - (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, + (nint)CSResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); _incRefHook.Enable(); _decRefHook = interop.HookFromAddress( - (nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, + (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); _decRefHook.Enable(); } @@ -41,6 +43,9 @@ public unsafe class ResourceService : IDisposable &category, &type, &hash, path.Path, null, false); } + public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, ByteString path) + => new((CSResourceHandle*)GetResource(category, type, path), false); + public void Dispose() { _getResourceSyncHook.Dispose(); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index d1701f47..615ef2b0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -9,6 +9,7 @@ using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; @@ -144,6 +145,14 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Imc, 0, imc, path); } + public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd) + { + if (pbd == null) + return null; + + return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath); + } + public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { if (tex == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 24112a9f..15546ed3 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.Objects.Enums; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -6,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Services; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; @@ -155,6 +155,22 @@ public class ResourceTree { var genericContext = globalContext.CreateContext(&human->CharacterBase); + var cache = globalContext.Collection._cache; + if (cache != null && cache.LoadedResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) + { + var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle); + if (pbdNode != null) + { + if (globalContext.WithUiData) + { + pbdNode = pbdNode.Clone(); + pbdNode.FallbackName = "Racial Deformer"; + pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + } + Nodes.Add(pbdNode); + } + } + var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); var decalPath = decalId != 0 ? GamePaths.Human.Decal.FaceDecalPath(decalId) diff --git a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs index 1f788a39..a5e73867 100644 --- a/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs +++ b/Penumbra/Interop/SafeHandles/SafeResourceHandle.cs @@ -1,8 +1,8 @@ -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; namespace Penumbra.Interop.SafeHandles; -public unsafe class SafeResourceHandle : SafeHandle +public unsafe class SafeResourceHandle : SafeHandle, ICloneable { public ResourceHandle* ResourceHandle => (ResourceHandle*)handle; @@ -21,6 +21,12 @@ public unsafe class SafeResourceHandle : SafeHandle SetHandle((nint)handle); } + public SafeResourceHandle Clone() + => new(ResourceHandle, true); + + object ICloneable.Clone() + => Clone(); + public static SafeResourceHandle CreateInvalid() => new(null, false); diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 699b59e0..da04bf90 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -28,6 +28,7 @@ public unsafe class CharacterUtility : IDisposable public bool Ready { get; private set; } public event Action LoadingFinished; + public nint DefaultHumanPbdResource { get; private set; } public nint DefaultTransparentResource { get; private set; } public nint DefaultDecalResource { get; private set; } public nint DefaultSkinShpkResource { get; private set; } @@ -88,6 +89,12 @@ public unsafe class CharacterUtility : IDisposable anyMissing |= !_lists[i].Ready; } + if (DefaultHumanPbdResource == nint.Zero) + { + DefaultHumanPbdResource = (nint)Address->HumanPbdResource; + anyMissing |= DefaultHumanPbdResource == nint.Zero; + } + if (DefaultTransparentResource == nint.Zero) { DefaultTransparentResource = (nint)Address->TransparentTexResource; @@ -151,6 +158,7 @@ public unsafe class CharacterUtility : IDisposable foreach (var list in _lists) list.Dispose(); + Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; diff --git a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs new file mode 100644 index 00000000..44c6541e --- /dev/null +++ b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs @@ -0,0 +1,95 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public sealed unsafe class PreBoneDeformerReplacer : IDisposable +{ + public static readonly Utf8GamePath PreBoneDeformerPath = + Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty; + + [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] + private readonly nint* _humanVTable = null!; + + // Approximate name guesses. + private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + + private readonly Hook _humanSetupScalingHook; + private readonly Hook _humanCreateDeformerHook; + + private readonly CharacterUtility _utility; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceLoader _resourceLoader; + private readonly IFramework _framework; + + public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, IGameInteropProvider interop, IFramework framework) + { + interop.InitializeFromAttributes(this); + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = interop.HookFromAddress(_humanVTable[57], SetupScaling); + _humanCreateDeformerHook = interop.HookFromAddress(_humanVTable[91], CreateDeformer); + _humanSetupScalingHook.Enable(); + _humanCreateDeformerHook.Enable(); + } + + public void Dispose() + { + _humanCreateDeformerHook.Dispose(); + _humanSetupScalingHook.Dispose(); + } + + private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) + { + var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); + return _resourceLoader.LoadCacheableSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); + } + + private void SetupScaling(CharacterBase* drawObject, uint slotIndex) + { + if (!_framework.IsInFrameworkUpdateThread) + Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + + using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + try + { + if (!preBoneDeformer.IsInvalid) + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; + _humanSetupScalingHook.Original(drawObject, slotIndex); + } + finally + { + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + } + } + + private void* CreateDeformer(CharacterBase* drawObject, uint slotIndex) + { + if (!_framework.IsInFrameworkUpdateThread) + Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + + using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + try + { + if (!preBoneDeformer.IsInvalid) + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; + return _humanCreateDeformerHook.Original(drawObject, slotIndex); + } + finally + { + _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + } + } +} diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 08857292..22150cc1 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,6 +5,7 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { + public const int IndexHumanPbd = 63; public const int IndexTransparentTex = 72; public const int IndexDecalTex = 73; public const int IndexSkinShpk = 76; @@ -72,6 +73,9 @@ public unsafe struct CharacterUtilityData public ResourceHandle* EqdpResource(GenderRace raceCode, bool accessory) => Resource((int)EqdpIdx(raceCode, accessory)); + [FieldOffset(8 + IndexHumanPbd * 8)] + public ResourceHandle* HumanPbdResource; + [FieldOffset(8 + (int)MetaIndex.HumanCmp * 8)] public ResourceHandle* HumanCmpResource; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ff068928..c34a7771 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,6 +77,7 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); + _services.GetService(); _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 66c90e84..6f85ddd2 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -132,6 +132,7 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) From 3066bf84d5fc4398e642d21c0449ba7a2300e6c2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Tue, 26 Mar 2024 02:32:33 +0100 Subject: [PATCH 005/865] Where did these `using`s even come from? --- Penumbra/Collections/Cache/CollectionCache.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index c2c215aa..00968175 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -9,8 +9,6 @@ using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Interop.SafeHandles; -using System.IO; -using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Collections.Cache; From e793e7793b1e4eeae991ba0beb4f7f49745b2bd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 15:19:19 +0100 Subject: [PATCH 006/865] Fix model import resetting dirty flag. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 3672dce7..737c41d9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -35,39 +35,28 @@ public partial class ModEditWindow private void UpdateFile(MdlFile file, bool force) { - if (file == _lastFile) + if (file == _lastFile && !force) + return; + + _lastFile = file; + var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); + if (_subMeshAttributeTagWidgets.Count != subMeshTotal) { - if (force) - UpdateMeshes(); - } - else - { - UpdateMeshes(); - _lastFile = file; - _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + _subMeshAttributeTagWidgets.Clear(); + _subMeshAttributeTagWidgets.AddRange( + Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) + ); } - return; - - void UpdateMeshes() - { - var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_subMeshAttributeTagWidgets.Count != subMeshTotal) - { - _subMeshAttributeTagWidgets.Clear(); - _subMeshAttributeTagWidgets.AddRange( - Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) - ); - } - } + _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); } private bool DrawModelPanel(MdlTab tab, bool disabled) { - UpdateFile(tab.Mdl, tab.Dirty); + var ret = tab.Dirty; + UpdateFile(tab.Mdl, ret); DrawImportExport(tab, disabled); - var ret = tab.Dirty; ret |= DrawModelMaterialDetails(tab, disabled); if (ImGui.CollapsingHeader($"Meshes ({_lastFile.Meshes.Length})###meshes")) From 1ba5011bfa5120821a30a39a8953f33329a59bc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 17:37:17 +0100 Subject: [PATCH 007/865] Small changes. --- Penumbra/Collections/Cache/CollectionCache.cs | 65 +++++++++---------- .../Cache/CollectionCacheManager.cs | 8 ++- .../Collections/Cache/CustomResourceCache.cs | 49 ++++++++++++++ .../Interop/ResourceLoading/ResourceLoader.cs | 18 ----- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- .../Services/PreBoneDeformerReplacer.cs | 34 +++++----- .../Services/ShaderReplacementFixer.cs | 33 +++++----- Penumbra/Penumbra.cs | 2 - Penumbra/Services/StaticServiceManager.cs | 4 +- 9 files changed, 120 insertions(+), 95 deletions(-) create mode 100644 Penumbra/Collections/Cache/CustomResourceCache.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 00968175..6b2b688b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -8,7 +8,6 @@ using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; -using Penumbra.Interop.SafeHandles; namespace Penumbra.Collections.Cache; @@ -19,16 +18,16 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// The Cache contains all required temporary data to use a collection. /// It will only be setup if a collection gets activated in any way. /// -public class CollectionCache : IDisposable +public sealed class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly ConcurrentDictionary LoadedResources = new(); - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, object?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly CustomResourceCache CustomResources; + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -39,7 +38,7 @@ public class CollectionCache : IDisposable => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -56,16 +55,21 @@ public class CollectionCache : IDisposable // The cache reacts through events on its collection changing. public CollectionCache(CollectionCacheManager manager, ModCollection collection) { - _manager = manager; - _collection = collection; - Meta = new MetaCache(manager.MetaFileManager, _collection); + _manager = manager; + _collection = collection; + Meta = new MetaCache(manager.MetaFileManager, _collection); + CustomResources = new CustomResourceCache(manager.ResourceLoader); } public void Dispose() - => Meta.Dispose(); + { + Meta.Dispose(); + CustomResources.Dispose(); + GC.SuppressFinalize(this); + } ~CollectionCache() - => Meta.Dispose(); + => Dispose(); // Resolve a given game path according to this collection. public FullPath? ResolvePath(Utf8GamePath gameResourcePath) @@ -74,7 +78,7 @@ public class CollectionCache : IDisposable return null; if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists) + || candidate.Path is { IsRooted: true, Exists: false }) return null; return candidate.Path; @@ -102,7 +106,7 @@ public class CollectionCache : IDisposable public HashSet[] ReverseResolvePaths(IReadOnlyCollection fullPaths) { if (fullPaths.Count == 0) - return Array.Empty>(); + return []; var ret = new HashSet[fullPaths.Count]; var dict = new Dictionary(fullPaths.Count); @@ -110,8 +114,8 @@ public class CollectionCache : IDisposable { dict[new FullPath(path)] = idx; ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8) - ? new HashSet { utf8 } - : new HashSet(); + ? [utf8] + : []; } foreach (var (game, full) in ResolvedFiles) @@ -138,13 +142,6 @@ public class CollectionCache : IDisposable public void RemoveMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModRemoval(this, mod, addMetaChanges)); - /// Invalidates caches subsequently to a resolved file being modified. - private void InvalidateResolvedFile(Utf8GamePath path) - { - if (LoadedResources.Remove(path, out var handle)) - handle.Dispose(); - } - /// Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFileSync(Utf8GamePath path, FullPath fullPath) { @@ -157,20 +154,20 @@ public class CollectionCache : IDisposable if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, Mod.ForcedFiles); } else { - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); } } else if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); } } @@ -193,7 +190,7 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.Remove(path, out var mp)) { - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); if (mp.Mod != mod) Penumbra.Log.Warning( $"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); @@ -308,7 +305,7 @@ public class CollectionCache : IDisposable if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { ModData.AddPath(mod, path); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -323,14 +320,14 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, path); - InvalidateResolvedFile(path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); } } catch (Exception ex) { Penumbra.Log.Error( - $"[{Thread.CurrentThread.ManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); + $"[{Environment.CurrentManagedThreadId}] Error adding redirection {file} -> {path} for mod {mod.Name} to collection cache {AnonymizedName}:\n{ex}"); } } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 94b3ef5a..5a6b5593 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -4,6 +4,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -21,8 +22,8 @@ public class CollectionCacheManager : IDisposable private readonly CollectionStorage _storage; private readonly ActiveCollections _active; internal readonly ResolvedFileChanged ResolvedFileChanged; - - internal readonly MetaFileManager MetaFileManager; + internal readonly MetaFileManager MetaFileManager; + internal readonly ResourceLoader ResourceLoader; private readonly ConcurrentQueue _changeQueue = new(); @@ -35,7 +36,7 @@ public class CollectionCacheManager : IDisposable => _storage.Where(c => c.HasCache); public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, - MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage) + MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader) { _framework = framework; _communicator = communicator; @@ -44,6 +45,7 @@ public class CollectionCacheManager : IDisposable MetaFileManager = metaFileManager; _active = active; _storage = storage; + ResourceLoader = resourceLoader; ResolvedFileChanged = _communicator.ResolvedFileChanged; if (!_active.Individuals.IsLoaded) diff --git a/Penumbra/Collections/Cache/CustomResourceCache.cs b/Penumbra/Collections/Cache/CustomResourceCache.cs new file mode 100644 index 00000000..46c28393 --- /dev/null +++ b/Penumbra/Collections/Cache/CustomResourceCache.cs @@ -0,0 +1,49 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.SafeHandles; +using Penumbra.String.Classes; + +namespace Penumbra.Collections.Cache; + +/// A cache for resources owned by a collection. +public sealed class CustomResourceCache(ResourceLoader loader) + : ConcurrentDictionary, IDisposable +{ + /// Invalidate an existing resource by clearing it from the cache and disposing it. + public void Invalidate(Utf8GamePath path) + { + if (TryRemove(path, out var handle)) + handle.Dispose(); + } + + public void Dispose() + { + foreach (var handle in Values) + handle.Dispose(); + Clear(); + } + + /// Get the requested resource either from the cached resource, or load a new one if it does not exist. + public SafeResourceHandle Get(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData data) + { + if (TryGetClonedValue(path, out var handle)) + return handle; + + handle = loader.LoadResolvedSafeResource(category, type, path.Path, data); + var clone = handle.Clone(); + if (!TryAdd(path, clone)) + clone.Dispose(); + return handle; + } + + /// Get a cloned cached resource if it exists. + private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle) + { + if (!TryGetValue(path, out handle)) + return false; + + handle = handle.Clone(); + return true; + } +} diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 1b467a8c..6c2c83b3 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -49,24 +49,6 @@ public unsafe class ResourceLoader : IDisposable return ret; } - public SafeResourceHandle LoadCacheableSafeResource(ResourceCategory category, ResourceType type, Utf8GamePath path, ResolveData resolveData) - { - var cache = resolveData.ModCollection._cache; - if (cache == null) - return LoadResolvedSafeResource(category, type, path.Path, resolveData); - - if (cache.LoadedResources.TryGetValue(path, out var cached)) - return cached.Clone(); - - var ret = LoadResolvedSafeResource(category, type, path.Path, resolveData); - - cached = ret.Clone(); - if (!cache.LoadedResources.TryAdd(path, cached)) - cached.Dispose(); - - return ret; - } - /// The function to use to resolve a given path. public Func ResolvePath = null!; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 15546ed3..dac86e44 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -156,7 +156,7 @@ public class ResourceTree var genericContext = globalContext.CreateContext(&human->CharacterBase); var cache = globalContext.Collection._cache; - if (cache != null && cache.LoadedResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) + if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) { var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle); if (pbdNode != null) diff --git a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs index 44c6541e..9f553257 100644 --- a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs @@ -1,10 +1,9 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.SafeHandles; @@ -12,14 +11,11 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.Services; -public sealed unsafe class PreBoneDeformerReplacer : IDisposable +public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService { public static readonly Utf8GamePath PreBoneDeformerPath = Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty; - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - // Approximate name guesses. private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); @@ -32,15 +28,16 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable private readonly ResourceLoader _resourceLoader; private readonly IFramework _framework; - public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, IGameInteropProvider interop, IFramework framework) + public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, + IGameInteropProvider interop, IFramework framework, CharacterBaseVTables vTables) { interop.InitializeFromAttributes(this); - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = interop.HookFromAddress(_humanVTable[57], SetupScaling); - _humanCreateDeformerHook = interop.HookFromAddress(_humanVTable[91], CreateDeformer); + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = interop.HookFromAddress(vTables.HumanVTable[57], SetupScaling); + _humanCreateDeformerHook = interop.HookFromAddress(vTables.HumanVTable[91], CreateDeformer); _humanSetupScalingHook.Enable(); _humanCreateDeformerHook.Enable(); } @@ -54,13 +51,17 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) { var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); - return _resourceLoader.LoadCacheableSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); + if (resolveData.ModCollection._cache is not { } cache) + return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData); + + return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } private void SetupScaling(CharacterBase* drawObject, uint slotIndex) { if (!_framework.IsInFrameworkUpdateThread) - Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + Penumbra.Log.Warning( + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try @@ -78,7 +79,8 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable private void* CreateDeformer(CharacterBase* drawObject, uint slotIndex) { if (!_framework.IsInFrameworkUpdateThread) - Penumbra.Log.Warning($"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + Penumbra.Log.Warning( + $"{nameof(PreBoneDeformerReplacer)}.{nameof(CreateDeformer)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs index 26906ace..3809ecbd 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; @@ -13,7 +14,7 @@ using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRen namespace Penumbra.Interop.Services; -public sealed unsafe class ShaderReplacementFixer : IDisposable +public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredService { public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; @@ -21,11 +22,10 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable public static ReadOnlySpan CharacterGlassShpkName => "characterglass.shpk"u8; - [Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)] - private readonly nint* _humanVTable = null!; - private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param); - private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); + + private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, + CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); private readonly Hook _humanOnRenderMaterialHook; @@ -59,14 +59,15 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable => _moddedCharacterGlassShpkCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, IGameInteropProvider interop) + CommunicatorService communicator, IGameInteropProvider interop, CharacterBaseVTables vTables) { interop.InitializeFromAttributes(this); - _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _modelRenderer = modelRenderer; - _communicator = communicator; - _humanOnRenderMaterialHook = interop.HookFromAddress(_humanVTable[62], OnRenderHumanMaterial); + _resourceHandleDestructor = resourceHandleDestructor; + _utility = utility; + _modelRenderer = modelRenderer; + _communicator = communicator; + _humanOnRenderMaterialHook = + interop.HookFromAddress(vTables.HumanVTable[62], OnRenderHumanMaterial); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); _humanOnRenderMaterialHook.Enable(); @@ -82,7 +83,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable _moddedCharacterGlassShpkMaterials.Clear(); _moddedSkinShpkMaterials.Clear(); _moddedCharacterGlassShpkCount = 0; - _moddedSkinShpkCount = 0; + _moddedSkinShpkCount = 0; } public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() @@ -106,16 +107,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable var shpkName = mtrl->ShpkNameSpan; if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) - { if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) Interlocked.Increment(ref _moddedSkinShpkCount); - } if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) - { if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) Interlocked.Increment(ref _moddedCharacterGlassShpkCount); - } } private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) @@ -159,9 +156,9 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable } } - private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) + private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, + CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { - // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. if (!Enabled || _moddedCharacterGlassShpkCount == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c34a7771..b76780c0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,8 +77,6 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); - _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 6f85ddd2..9e6071b4 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -131,9 +131,7 @@ public static class StaticServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton() From b04cb343dd5aacd4a6134ad28e7dca1154cce979 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 19:32:10 +0100 Subject: [PATCH 008/865] Make setting for crash handler. --- Penumbra/Configuration.cs | 18 +++--- Penumbra/Services/CrashHandlerService.cs | 6 +- Penumbra/UI/Tabs/SettingsTab.cs | 77 +++++++++++++++--------- 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9c0b4f2d..f91e0534 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -33,12 +33,12 @@ public class Configuration : IPluginConfiguration, ISavable public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public bool UseCrashHandler { get; set; } = true; - public bool OpenWindowAtStart { get; set; } = false; - public bool HideUiInGPose { get; set; } = false; - public bool HideUiInCutscenes { get; set; } = true; - public bool HideUiWhenUiHidden { get; set; } = false; - public bool UseDalamudUiTextureRedirection { get; set; } = true; + public bool? UseCrashHandler { get; set; } = null; + public bool OpenWindowAtStart { get; set; } = false; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiWhenUiHidden { get; set; } = false; + public bool UseDalamudUiTextureRedirection { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; @@ -48,9 +48,9 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index c713d623..6025c2c0 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -36,7 +36,7 @@ public sealed class CrashHandlerService : IDisposable, IService _config = config; _validityChecker = validityChecker; - if (!_config.UseCrashHandler) + if (_config.UseCrashHandler ?? false) return; OpenEventWriter(); @@ -84,7 +84,7 @@ public sealed class CrashHandlerService : IDisposable, IService public void Enable() { - if (_config.UseCrashHandler) + if (_config.UseCrashHandler ?? false) return; _config.UseCrashHandler = true; @@ -97,7 +97,7 @@ public sealed class CrashHandlerService : IDisposable, IService public void Disable() { - if (!_config.UseCrashHandler) + if (!(_config.UseCrashHandler ?? false)) return; _config.UseCrashHandler = false; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 80fe6fb6..c524a840 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -43,6 +43,7 @@ public class SettingsTab : ITab private readonly DalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly PredefinedTagManager _predefinedTagManager; + private readonly CrashHandlerService _crashService; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -53,7 +54,7 @@ public class SettingsTab : ITab Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData, PredefinedTagManager predefinedTagConfig) + IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService) { _pluginInterface = pluginInterface; _config = config; @@ -74,6 +75,7 @@ public class SettingsTab : ITab if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; _predefinedTagManager = predefinedTagConfig; + _crashService = crashService; } public void DrawHeader() @@ -228,35 +230,35 @@ public class SettingsTab : ITab _newModDirectory = _config.ModDirectory; bool save, selected; - using (ImRaii.Group()) - { - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) - { - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) - .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); - save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, - RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); - } - - selected = ImGui.IsItemActive(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - - const string tt = "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; - ImGuiComponents.HelpMarker(tt); - _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); - ImGui.SameLine(); - ImGui.TextUnformatted("Root Directory"); - ImGuiUtil.HoverTooltip(tt); + using (ImRaii.Group()) + { + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) + { + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) + .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); + save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, + RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + } + + selected = ImGui.IsItemActive(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker(tt); + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); + ImGui.SameLine(); + ImGui.TextUnformatted("Root Directory"); + ImGuiUtil.HoverTooltip(tt); } _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); @@ -704,6 +706,7 @@ public class SettingsTab : ITab if (!header) return; + DrawCrashHandler(); DrawMinimumDimensionConfig(); Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", @@ -721,6 +724,20 @@ public class SettingsTab : ITab ImGui.NewLine(); } + private void DrawCrashHandler() + { + Checkbox("Enable Penumbra Crash Logging (Experimental)", + "Enables Penumbra to launch a secondary process that records some game activity which may or may not help diagnosing Penumbra-related game crashes.", + _config.UseCrashHandler ?? false, + v => + { + if (v) + _crashService.Enable(); + else + _crashService.Disable(); + }); + } + private void DrawCompressionBox() { if (!_compactor.CanCompact) From 47c5187ad929468610f49ae04a4f534e21dc4a21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 28 Mar 2024 19:34:30 +0100 Subject: [PATCH 009/865] Derp. --- Penumbra/Services/CrashHandlerService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 6025c2c0..5423ec15 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -36,7 +36,7 @@ public sealed class CrashHandlerService : IDisposable, IService _config = config; _validityChecker = validityChecker; - if (_config.UseCrashHandler ?? false) + if (!(_config.UseCrashHandler ?? false)) return; OpenEventWriter(); From a39419288c1432f5fa9a9a7e1b7b97a8e48ea574 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 28 Mar 2024 18:36:31 +0000 Subject: [PATCH 010/865] [CI] Updating repo.json for testing_1.0.2.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 5a51afa2..936adfc0 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.5", + "TestingAssemblyVersion": "1.0.2.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,7 +17,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From de239578ccb83137d2c4f69ad2b7bd8781d686d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Mar 2024 14:44:08 +0100 Subject: [PATCH 011/865] Fix weird exception. --- Penumbra/Api/IpcTester.cs | 76 +++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 30f3c80f..898c5de3 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -583,23 +583,18 @@ public class IpcTester : IDisposable } } - private class Resolve + private class Resolve(DalamudPluginInterface pi) { - private readonly DalamudPluginInterface _pi; - private string _currentResolvePath = string.Empty; private string _currentResolveCharacter = string.Empty; private string _currentReversePath = string.Empty; private int _currentReverseIdx; - private Task<(string[], string[][])> _task = Task.FromException<(string[], string[][])>(new Exception()); - - public Resolve(DalamudPluginInterface pi) - => _pi = pi; + private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], [])); public void Draw() { - using var _ = ImRaii.TreeNode("Resolving"); - if (!_) + using var tree = ImRaii.TreeNode("Resolving"); + if (!tree) return; ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); @@ -613,28 +608,28 @@ public class IpcTester : IDisposable DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(_pi).Invoke(_currentResolvePath)); + ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(pi).Invoke(_currentResolvePath)); DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(_pi).Invoke(_currentResolvePath)); + ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(pi).Invoke(_currentResolvePath)); DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(_pi).Invoke(_currentResolvePath)); + ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(pi).Invoke(_currentResolvePath)); DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve"); if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentResolveCharacter)); + ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(pi).Invoke(_currentResolvePath, _currentResolveCharacter)); DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve"); if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(_pi).Invoke(_currentResolvePath, _currentReverseIdx)); + ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(pi).Invoke(_currentResolvePath, _currentReverseIdx)); DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths"); if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolvePath.Subscriber(_pi).Invoke(_currentReversePath, _currentResolveCharacter); + var list = Ipc.ReverseResolvePath.Subscriber(pi).Invoke(_currentReversePath, _currentResolveCharacter); if (list.Length > 0) { ImGui.TextUnformatted(list[0]); @@ -646,7 +641,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolvePlayerPath.Subscriber(_pi).Invoke(_currentReversePath); + var list = Ipc.ReverseResolvePlayerPath.Subscriber(pi).Invoke(_currentReversePath); if (list.Length > 0) { ImGui.TextUnformatted(list[0]); @@ -658,7 +653,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); if (_currentReversePath.Length > 0) { - var list = Ipc.ReverseResolveGameObjectPath.Subscriber(_pi).Invoke(_currentReversePath, _currentReverseIdx); + var list = Ipc.ReverseResolveGameObjectPath.Subscriber(pi).Invoke(_currentReversePath, _currentReverseIdx); if (list.Length > 0) { ImGui.TextUnformatted(list[0]); @@ -668,19 +663,31 @@ public class IpcTester : IDisposable } var forwardArray = _currentResolvePath.Length > 0 - ? new[] - { - _currentResolvePath, - } + ? [_currentResolvePath] : Array.Empty(); var reverseArray = _currentReversePath.Length > 0 - ? new[] - { - _currentReversePath, - } + ? [_currentReversePath] : Array.Empty(); - string ConvertText((string[], string[][]) data) + DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); + if (forwardArray.Length > 0 || reverseArray.Length > 0) + { + var ret = Ipc.ResolvePlayerPaths.Subscriber(pi).Invoke(forwardArray, reverseArray); + ImGui.TextUnformatted(ConvertText(ret)); + } + + DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); + if (ImGui.Button("Start")) + _task = Ipc.ResolvePlayerPathsAsync.Subscriber(pi).Invoke(forwardArray, reverseArray); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(_task.Status.ToString()); + if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) + ImGui.SetTooltip(ConvertText(_task.Result)); + return; + + static string ConvertText((string[], string[][]) data) { var text = string.Empty; if (data.Item1.Length > 0) @@ -697,23 +704,6 @@ public class IpcTester : IDisposable return text; } - - DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); - if (forwardArray.Length > 0 || reverseArray.Length > 0) - { - var ret = Ipc.ResolvePlayerPaths.Subscriber(_pi).Invoke(forwardArray, reverseArray); - ImGui.TextUnformatted(ConvertText(ret)); - } - - DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); - if (ImGui.Button("Start")) - _task = Ipc.ResolvePlayerPathsAsync.Subscriber(_pi).Invoke(forwardArray, reverseArray); - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_task.Status.ToString()); - if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) - ImGui.SetTooltip(ConvertText(_task.Result)); } } From b4b813fe5e05ef321cda034de6691d58cc8d5b9d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 29 Mar 2024 20:05:25 +0100 Subject: [PATCH 012/865] Advanced Editing minor improvements --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../UI/AdvancedWindow/ModEditWindow.Materials.cs | 3 ++- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- .../AdvancedWindow/ModEditWindow.ShaderPackages.cs | 12 +++++++++--- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 4e06921d..b70c7cc2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4e06921da239788331a4527aa6a2943cf0e809fe +Subproject commit b70c7cc2f6c71f80884de30a237cab201d7fe150 diff --git a/Penumbra.GameData b/Penumbra.GameData index 66687643..45679aa3 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 66687643da2163c938575ad6949c8d0fbd03afe7 +Subproject commit 45679aa32cc37b59f5eeb7cf6bf5a3ea36c626e0 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index fab41c7d..68b3717f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -170,7 +171,7 @@ public partial class ModEditWindow using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', file.AdditionalData.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(file.AdditionalData); } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 737c41d9..03f276ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -560,7 +560,7 @@ public partial class ModEditWindow { using var t = ImRaii.TreeNode($"Additional Data (Size: {_lastFile.RemainingData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', _lastFile.RemainingData.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(_lastFile.RemainingData); } return false; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 8d1c8cb7..070895b5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; +using OtterGui.Widgets; namespace Penumbra.UI.AdvancedWindow; @@ -172,11 +173,16 @@ public partial class ModEditWindow ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); - if (shader.AdditionalHeader.Length > 0) + if (shader.DeclaredInputs != 0) + ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + if (shader.UsedInputs != 0) + ImRaii.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + + if (shader.AdditionalHeader.Length > 8) { using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); if (t2) - ImGuiUtil.TextWrapped(string.Join(' ', shader.AdditionalHeader.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(shader.AdditionalHeader); } if (tab.Shpk.Disassembled) @@ -549,7 +555,7 @@ public partial class ModEditWindow { using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); if (t) - ImGuiUtil.TextWrapped(string.Join(' ', tab.Shpk.AdditionalData.Select(c => $"{c:X2}"))); + Widget.DrawHexViewer(tab.Shpk.AdditionalData); } } From 5cebddb0ab680fa38edb62e3b5cc6cfe23203388 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 30 Mar 2024 14:59:31 +0100 Subject: [PATCH 013/865] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b70c7cc2..f641a34f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b70c7cc2f6c71f80884de30a237cab201d7fe150 +Subproject commit f641a34ffa80e89bd61701f60f15d15c4c5b361e From a65009dfb0f7714a4343c44d9f0dd0dc61ee0760 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 1 Apr 2024 13:59:09 +0200 Subject: [PATCH 014/865] Fix issue with merging and deduplicating. --- OtterGui | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 16 ++++--- Penumbra/Mods/Editor/ModMerger.cs | 58 ++++++++++++------------ Penumbra/Mods/Manager/ModOptionEditor.cs | 31 +++++++------ Penumbra/Services/CrashHandlerService.cs | 2 +- 5 files changed, 59 insertions(+), 50 deletions(-) diff --git a/OtterGui b/OtterGui index f641a34f..f48c6886 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f641a34ffa80e89bd61701f60f15d15c4c5b361e +Subproject commit f48c6886cbc163c5a292fa8b9fd919cb01c11d7b diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 77d10cc4..c8530936 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -81,7 +82,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (useModManager) { - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync); } else { @@ -216,18 +217,21 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } /// Deduplicate a mod simply by its directory without any confirmation or waiting time. - internal void DeduplicateMod(DirectoryInfo modDirectory) + internal void DeduplicateMod(DirectoryInfo modDirectory, bool useModManager = false) { try { - var mod = new Mod(modDirectory); - modManager.Creator.ReloadMod(mod, true, out _); + if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod)) + { + mod = new Mod(modDirectory); + modManager.Creator.ReloadMod(mod, true, out _); + } Clear(); var files = new ModFileCollection(); files.UpdateAll(mod, mod.Default); - CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray(), CancellationToken.None); - DeleteDuplicates(files, mod, mod.Default, false); + CheckDuplicates([.. files.Available.OrderByDescending(f => f.FileSize)], CancellationToken.None); + DeleteDuplicates(files, mod, mod.Default, useModManager); } catch (Exception e) { diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index f5d0e4a4..842b1bb3 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -36,7 +36,7 @@ public class ModMerger : IDisposable public readonly HashSet SelectedOptions = []; - public readonly IReadOnlyList Warnings = new List(); + public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, @@ -78,7 +78,8 @@ public class ModMerger : IDisposable MergeWithOptions(); else MergeIntoOption(OptionGroupName, OptionName); - _duplicates.DeduplicateMod(MergeToMod.ModPath); + + _duplicates.DeduplicateMod(MergeToMod.ModPath, true); } catch (Exception ex) { @@ -134,10 +135,10 @@ public class ModMerger : IDisposable return; } - var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName); + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); + var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) _createdOptions.Add(option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); @@ -156,27 +157,6 @@ public class ModMerger : IDisposable var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var manips = option.ManipulationData.ToHashSet(); - bool GetFullPath(FullPath input, out FullPath ret) - { - if (fromFileToFile) - { - if (!_fileToFile.TryGetValue(input.FullName, out var s)) - { - ret = input; - return false; - } - - ret = new FullPath(s); - return true; - } - - if (!Utf8RelPath.FromFile(input, MergeFromMod!.ModPath, out var relPath)) - throw new Exception($"Could not create relative path from {input} and {MergeFromMod!.ModPath}."); - - ret = new FullPath(MergeToMod!.ModPath, relPath); - return true; - } - foreach (var originalOption in mergeOptions) { foreach (var manip in originalOption.Manipulations) @@ -204,9 +184,31 @@ public class ModMerger : IDisposable } } - _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections); - _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps); - _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips); + _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips, SaveType.ImmediateSync); + return; + + bool GetFullPath(FullPath input, out FullPath ret) + { + if (fromFileToFile) + { + if (!_fileToFile.TryGetValue(input.FullName, out var s)) + { + ret = input; + return false; + } + + ret = new FullPath(s); + return true; + } + + if (!Utf8RelPath.FromFile(input, MergeFromMod!.ModPath, out var relPath)) + throw new Exception($"Could not create relative path from {input} and {MergeFromMod!.ModPath}."); + + ret = new FullPath(MergeToMod!.ModPath, relPath); + return true; + } } private void CopyFiles(DirectoryInfo directory) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 3459ce1a..60508d33 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -78,7 +78,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new, empty option group of the given type and name. - public void AddModGroup(Mod mod, GroupType type, string newName) + public void AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) { if (!VerifyFileName(mod, null, newName, true)) return; @@ -96,18 +96,18 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Name = newName, Priority = maxPriority, }); - saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } /// Add a new mod, empty option group of the given type and name if it does not exist already. - public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName) + public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) { var idx = mod.Groups.IndexOf(g => g.Name == newName); if (idx >= 0) return (mod.Groups[idx], idx, false); - AddModGroup(mod, type, newName); + AddModGroup(mod, type, newName, saveType); if (mod.Groups[^1].Name != newName) throw new Exception($"Could not create new mod group with name {newName}."); @@ -226,7 +226,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new empty option of the given name for the given group. - public void AddOption(Mod mod, int groupIdx, string newName) + public void AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; var subMod = new SubMod(mod) { Name = newName }; @@ -241,19 +241,19 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; } - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName) + public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; var idx = group.IndexOf(o => o.Name == newName); if (idx >= 0) return ((SubMod)group[idx], false); - AddOption(mod, groupIdx, newName); + AddOption(mod, groupIdx, newName, saveType); if (group[^1].Name != newName) throw new Exception($"Could not create new option with name {newName} in {group.Name}."); @@ -324,7 +324,8 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations, + SaveType saveType = SaveType.Queue) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.Manipulations.Count == manipulations.Count @@ -333,12 +334,13 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.ManipulationData.SetTo(manipulations); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements) + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements, + SaveType saveType = SaveType.Queue) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.FileData.SetEquals(replacements)) @@ -346,7 +348,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileData.SetTo(replacements); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } @@ -364,7 +366,8 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps) + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps, + SaveType saveType = SaveType.Queue) { var subMod = GetSubMod(mod, groupIdx, optionIdx); if (subMod.FileSwapData.SetEquals(swaps)) @@ -372,7 +375,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileSwapData.SetTo(swaps); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 5423ec15..078b812b 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -213,7 +213,7 @@ public sealed class CrashHandlerService : IDisposable, IService } catch (Exception ex) { - Penumbra.Log.Debug($"Could not delete {dir}:\n{ex}"); + Penumbra.Log.Verbose($"Could not delete {dir}. This is generally not an error:\n{ex}"); } } } From 6e7512c13e20d0585f4d9c36aeb0c1563b62c568 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 14:53:30 +0200 Subject: [PATCH 015/865] Add Punchline. --- Penumbra/Penumbra.json | 1 + repo.json | 1 + 2 files changed, 2 insertions(+) diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 8173e001..85e01c84 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -1,6 +1,7 @@ { "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "9.0.0.1", diff --git a/repo.json b/repo.json index 936adfc0..232afaa0 100644 --- a/repo.json +++ b/repo.json @@ -2,6 +2,7 @@ { "Author": "Ottermandias, Adam, Wintermute", "Name": "Penumbra", + "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", From 77bf441e626a00a1d7e4b4de2c791472c2de66ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 15:29:19 +0200 Subject: [PATCH 016/865] Update Open Settings and Main UI. --- Penumbra/UI/ConfigWindow.cs | 7 +++++++ Penumbra/UI/WindowSystem.cs | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index d52ebb99..9ae11fc3 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using Penumbra.Api.Enums; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.Tabs; @@ -35,6 +36,12 @@ public sealed class ConfigWindow : Window IsOpen = _config.OpenWindowAtStart; } + public void OpenSettings() + { + _configTabs.SelectTab = TabType.Settings; + IsOpen = true; + } + public void Setup(Penumbra penumbra, ConfigTabBar configTabs) { _penumbra = penumbra; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 62ad5a6e..c5418eb3 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -27,7 +27,8 @@ public class PenumbraWindowSystem : IDisposable _windowSystem.AddWindow(editWindow); _windowSystem.AddWindow(importPopup); _windowSystem.AddWindow(debugTab); - _uiBuilder.OpenConfigUi += Window.Toggle; + _uiBuilder.OpenMainUi += Window.Toggle; + _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; @@ -40,7 +41,8 @@ public class PenumbraWindowSystem : IDisposable public void Dispose() { - _uiBuilder.OpenConfigUi -= Window.Toggle; + _uiBuilder.OpenMainUi -= Window.Toggle; + _uiBuilder.OpenConfigUi -= Window.OpenSettings; _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.Draw -= _fileDialog.Draw; } From b1ca073276e140ab88cc2121ee52b0a166af02dc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 16:35:55 +0200 Subject: [PATCH 017/865] Turn Settings and Priority into their own types. --- Penumbra/Api/PenumbraApi.cs | 49 ++++---- Penumbra/Api/TempModManager.cs | 21 ++-- Penumbra/Collections/Cache/CollectionCache.cs | 8 +- .../Cache/CollectionCacheManager.cs | 7 +- .../Collections/Manager/CollectionEditor.cs | 66 +++------- .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/ModCollectionMigration.cs | 4 +- Penumbra/Communication/ModSettingChanged.cs | 5 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 10 +- .../PathResolving/CollectionResolver.cs | 5 +- Penumbra/Mods/Editor/IMod.cs | 4 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 5 +- Penumbra/Mods/Mod.cs | 8 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Mods/Subclasses/IModGroup.cs | 7 +- Penumbra/Mods/Subclasses/ModPriority.cs | 61 ++++++++++ Penumbra/Mods/Subclasses/ModSettings.cs | 115 ++++++------------ Penumbra/Mods/Subclasses/MultiModGroup.cs | 22 ++-- Penumbra/Mods/Subclasses/Setting.cs | 62 ++++++++++ Penumbra/Mods/Subclasses/SettingList.cs | 57 +++++++++ Penumbra/Mods/Subclasses/SingleModGroup.cs | 32 ++--- Penumbra/Mods/TemporaryMod.cs | 7 +- Penumbra/Services/ConfigMigrationService.cs | 17 ++- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 18 +-- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 57 ++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 10 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 55 +++++---- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 29 files changed, 422 insertions(+), 298 deletions(-) create mode 100644 Penumbra/Mods/Subclasses/ModPriority.cs create mode 100644 Penumbra/Mods/Subclasses/Setting.cs create mode 100644 Penumbra/Mods/Subclasses/SettingList.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index da1eafd0..dc1e8472 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -27,6 +27,7 @@ using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; using Penumbra.Interop.ResourceTree; using Penumbra.Mods.Editor; +using Penumbra.Mods.Subclasses; namespace Penumbra.Api; @@ -39,13 +40,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi { add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); - } + } public event Action? PreSettingsPanelDraw { add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); - } + } public event Action? PostEnabledDraw { @@ -649,7 +650,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var shareSettings = settings.ConvertToShareable(mod); return (PenumbraApiEc.Success, - (shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[mod.Index] != null)); + (shareSettings.Enabled, shareSettings.Priority.Value, shareSettings.Settings, collection.Settings[mod.Index] != null)); } public PenumbraApiEc ReloadMod(string modDirectory, string modName) @@ -791,7 +792,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - return _collectionEditor.SetModPriority(collection, mod, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; + return _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority)) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; } public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, @@ -820,7 +823,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, "OptionName", optionName)); - var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; + var setting = mod.Groups[groupIdx].Type switch + { + GroupType.Multi => Setting.Multi(optionIdx), + GroupType.Single => Setting.Single(optionIdx), + _ => Setting.Zero, + }; return Return( _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, @@ -850,7 +858,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var group = mod.Groups[groupIdx]; - uint setting = 0; + var setting = Setting.Zero; if (group.Type == GroupType.Single) { var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); @@ -859,7 +867,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, "#optionNames", optionNames.Count.ToString())); - setting = (uint)optionIdx; + setting = Setting.Single(optionIdx); } else { @@ -871,7 +879,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, "#optionNames", optionNames.Count.ToString())); - setting |= 1u << optionIdx; + setting |= Setting.Multi(optionIdx); } } @@ -993,7 +1001,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return _tempMods.Register(tag, null, p, m, priority) switch + return _tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -1014,7 +1022,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return _tempMods.Register(tag, collection, p, m, priority) switch + return _tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -1024,7 +1032,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) { CheckInitialized(); - return _tempMods.Unregister(tag, null, priority) switch + return _tempMods.Unregister(tag, null, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -1039,7 +1047,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi && !_collectionManager.Storage.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - return _tempMods.Unregister(tag, collection, priority) switch + return _tempMods.Unregister(tag, collection, new ModPriority(priority)) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -1089,7 +1097,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int) index)).OfType(); + var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); @@ -1153,7 +1161,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) ? c : _collectionManager.Active.Individual(identifier); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -1161,7 +1169,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); AssociatedCollection(gameObjectIdx, out var collection); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -1190,7 +1198,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) + private ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) return ActorIdentifier.Invalid; @@ -1217,10 +1225,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if (Path.IsPathRooted(resolvedPath)) - return _lumina?.GetFileFromDisk(resolvedPath); - - return _gameData.GetFile(resolvedPath); + return Path.IsPathRooted(resolvedPath) + ? _lumina?.GetFileFromDisk(resolvedPath) + : _gameData.GetFile(resolvedPath); } catch (Exception e) { @@ -1295,7 +1302,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return _actors.CreatePlayer(b, worldId); } - private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int _1, int _2, bool inherited) + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index c7840b75..7d682338 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -6,6 +6,7 @@ using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Collections.Manager; using Penumbra.Communication; +using Penumbra.Mods.Subclasses; namespace Penumbra.Api; @@ -21,8 +22,8 @@ public class TempModManager : IDisposable { private readonly CommunicatorService _communicator; - private readonly Dictionary> _mods = new(); - private readonly List _modsForAllCollections = new(); + private readonly Dictionary> _mods = []; + private readonly List _modsForAllCollections = []; public TempModManager(CommunicatorService communicator) { @@ -42,7 +43,7 @@ public class TempModManager : IDisposable => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, - HashSet manips, int priority) + HashSet manips, ModPriority priority) { var mod = GetOrCreateMod(tag, collection, priority, out var created); Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); @@ -51,10 +52,10 @@ public class TempModManager : IDisposable return RedirectResult.Success; } - public RedirectResult Unregister(string tag, ModCollection? collection, int? priority) + public RedirectResult Unregister(string tag, ModCollection? collection, ModPriority? priority) { Penumbra.Log.Verbose($"Removing temporary mod with tag {tag}..."); - var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; + var list = collection == null ? _modsForAllCollections : _mods.GetValueOrDefault(collection); if (list == null) return RedirectResult.NotRegistered; @@ -85,13 +86,13 @@ public class TempModManager : IDisposable { Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}."); collection.Remove(mod); - _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, 0, 0, false); + _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false); } else { Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}."); collection.Apply(mod, created); - _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, 1, 0, false); + _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false); } } else @@ -116,7 +117,7 @@ public class TempModManager : IDisposable // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Returns the found or created mod and whether it was newly created. - private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created) + private TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, ModPriority priority, out bool created) { List list; if (collection == null) @@ -129,14 +130,14 @@ public class TempModManager : IDisposable } else { - list = new List(); + list = []; _mods.Add(collection, list); } var mod = list.Find(m => m.Priority == priority && m.Name == tag); if (mod == null) { - mod = new TemporaryMod() + mod = new TemporaryMod { Name = tag, Priority = priority, diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 6b2b688b..72f0fb59 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -237,7 +237,7 @@ public sealed class CollectionCache : IDisposable if (settings is not { Enabled: true }) return; - foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority)) + foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) { if (group.Count == 0) continue; @@ -246,13 +246,13 @@ public sealed class CollectionCache : IDisposable switch (group.Type) { case GroupType.Single: - AddSubMod(group[(int)config], mod); + AddSubMod(group[config.AsIndex], mod); break; case GroupType.Multi: { foreach (var (option, _) in group.WithIndex() - .Where(p => ((1 << p.Item2) & config) != 0) - .OrderByDescending(p => group.OptionPriority(p.Item2))) + .Where(p => config.HasFlag(p.Index)) + .OrderByDescending(p => group.OptionPriority(p.Index))) AddSubMod(option, mod); break; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 5a6b5593..f6c6e14a 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -8,6 +8,7 @@ using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.String.Classes; @@ -288,7 +289,7 @@ public class CollectionCacheManager : IDisposable MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } - private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _) { if (!collection.HasCache) return; @@ -300,9 +301,9 @@ public class CollectionCacheManager : IDisposable cache.ReloadMod(mod!, true); break; case ModSettingChange.EnableState: - if (oldValue == 0) + if (oldValue == Setting.False) cache.AddMod(mod!, true); - else if (oldValue == 1) + else if (oldValue == Setting.True) cache.RemoveMod(mod!, true); else if (collection[mod!.Index].Settings?.Enabled == true) cache.ReloadMod(mod!, true); diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 73950942..4af19e6b 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -7,26 +7,15 @@ using Penumbra.Services; namespace Penumbra.Collections.Manager; -public class CollectionEditor +public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) { - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; - private readonly ModStorage _modStorage; - - public CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) - { - _saveService = saveService; - _communicator = communicator; - _modStorage = modStorage; - } - /// Enable or disable the mod inheritance of mod idx. public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit) { if (!FixInheritance(collection, mod, inherit)) return false; - InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? 0 : 1, 0); + InvokeChange(collection, ModSettingChange.Inheritance, mod, inherit ? Setting.False : Setting.True, 0); return true; } @@ -42,7 +31,8 @@ public class CollectionEditor var inheritance = FixInheritance(collection, mod, false); ((List)collection.Settings)[mod.Index]!.Enabled = newValue; - InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? -1 : newValue ? 0 : 1, 0); + InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True, + 0); return true; } @@ -52,7 +42,7 @@ public class CollectionEditor if (!mods.Aggregate(false, (current, mod) => current | FixInheritance(collection, mod, inherit))) return; - InvokeChange(collection, ModSettingChange.MultiInheritance, null, -1, 0); + InvokeChange(collection, ModSettingChange.MultiInheritance, null, Setting.Indefinite, 0); } /// @@ -76,22 +66,22 @@ public class CollectionEditor if (!changes) return; - InvokeChange(collection, ModSettingChange.MultiEnableState, null, -1, 0); + InvokeChange(collection, ModSettingChange.MultiEnableState, null, Setting.Indefinite, 0); } /// /// Set the priority of mod idx to newValue if it differs from the current priority. /// If the mod is currently inherited, stop the inheritance. /// - public bool SetModPriority(ModCollection collection, Mod mod, int newValue) + public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue) { - var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? 0; + var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); ((List)collection.Settings)[mod.Index]!.Priority = newValue; - InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? -1 : oldValue, 0); + InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0); return true; } @@ -99,7 +89,7 @@ public class CollectionEditor /// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. /// /// If the mod is currently inherited, stop the inheritance. /// - public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, uint newValue) + public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { var settings = collection.Settings[mod.Index] != null ? collection.Settings[mod.Index]!.Settings @@ -110,7 +100,7 @@ public class CollectionEditor var inheritance = FixInheritance(collection, mod, false); ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue); - InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? -1 : (int)oldValue, groupIdx); + InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx); return true; } @@ -158,35 +148,17 @@ public class CollectionEditor if (savedSettings != null) { ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } else if (((Dictionary)collection.UnusedSettings).Remove(targetName)) { - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } } return true; } - /// - /// Change one of the available mod settings for mod idx discerned by type. - /// If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. - /// The setting will also be automatically fixed if it is invalid for that setting group. - /// For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - /// - public bool ChangeModSetting(ModCollection collection, ModSettingChange type, Mod mod, int newValue, int groupIdx) - { - return type switch - { - ModSettingChange.Inheritance => SetModInheritance(collection, mod, newValue != 0), - ModSettingChange.EnableState => SetModState(collection, mod, newValue != 0), - ModSettingChange.Priority => SetModPriority(collection, mod, newValue), - ModSettingChange.Setting => SetModSetting(collection, mod, groupIdx, (uint)newValue), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), - }; - } - /// /// Set inheritance of a mod without saving, /// to be used as an intermediary. @@ -204,16 +176,16 @@ public class CollectionEditor /// Queue saves and trigger changes for any non-inherited change in a collection, then trigger changes for all inheritors. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) + private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - _saveService.QueueSave(new ModCollectionSave(_modStorage, changedCollection)); - _communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); + saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); + communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } /// Trigger changes in all inherited collections. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, int oldValue, int groupIdx) + private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { foreach (var directInheritor in directParent.DirectParentOf) { @@ -221,11 +193,11 @@ public class CollectionEditor { case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - _communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); + communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); break; default: if (directInheritor.Settings[mod!.Index] == null) - _communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); + communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); break; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 0ee55376..d0b61e57 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -268,7 +268,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable case ModPathChangeType.Reloaded: foreach (var collection in this) { - if (collection.Settings[mod.Index]?.FixAllSettings(mod) ?? false) + if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index b2b8df0d..053f0a2b 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -40,9 +40,9 @@ internal static class ModCollectionMigration /// We treat every completely defaulted setting as inheritance-ready. private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); + => setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.Values.All(s => s == Setting.Zero); /// private static bool SettingIsDefaultV0(ModSettings? setting) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0); + => setting is { Enabled: true, Priority.IsDefault: true } && setting.Settings.All(s => s == Setting.Zero); } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 5e0bc0c0..412b3003 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -3,6 +3,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; +using Penumbra.Mods.Subclasses; namespace Penumbra.Communication; @@ -12,13 +13,13 @@ namespace Penumbra.Communication; /// Parameter is the collection in which the setting was changed. /// Parameter is the type of change. /// Parameter is the mod the setting was changed for, unless it was a multi-change. -/// Parameter is the old value of the setting before the change as int. +/// Parameter is the old value of the setting before the change as Setting. /// Parameter is the index of the changed group if the change type is Setting. /// Parameter is whether the change was inherited from another collection. /// /// public sealed class ModSettingChanged() - : EventWrapper(nameof(ModSettingChanged)) + : EventWrapper(nameof(ModSettingChanged)) { public enum Priority { diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7c4b94d8..7a247a53 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -174,7 +174,7 @@ public partial class TexToolsImporter ?? new DirectoryInfo(Path.Combine(_currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}")); - uint? defaultSettings = group.SelectionType == GroupType.Multi ? 0u : null; + Setting? defaultSettings = group.SelectionType == GroupType.Multi ? Setting.Zero : null; for (var i = 0; i + optionIdx < allOptions.Count && i < maxOptions; ++i) { var option = allOptions[i + optionIdx]; @@ -186,8 +186,8 @@ public partial class TexToolsImporter options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); if (option.IsChecked) defaultSettings = group.SelectionType == GroupType.Multi - ? defaultSettings!.Value | (1u << i) - : (uint)i; + ? defaultSettings!.Value | Setting.Multi(i) + : Setting.Single(i); ++_currentOptionIdx; } @@ -205,12 +205,12 @@ public partial class TexToolsImporter _currentOptionName = option.Name; options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); if (option.IsChecked) - defaultSettings = (uint) idx; + defaultSettings = Setting.Single(idx); } } _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, - defaultSettings ?? 0, group.Description, options); + defaultSettings ?? Setting.Zero, group.Description, options); ++groupPriority; } } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index fa122e39..aea58304 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -18,6 +18,7 @@ public sealed unsafe class CollectionResolver( PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, + ObjectManager objects, IGameGui gameGui, ActorManager actors, CutsceneService cutscenes, @@ -35,8 +36,8 @@ public sealed unsafe class CollectionResolver( public ModCollection PlayerCollection() { using var performance1 = performance.Measure(PerformanceType.IdentifyCollection); - var gameObject = (GameObject*)(clientState.LocalPlayer?.Address ?? nint.Zero); - if (gameObject == null) + var gameObject = objects[0]; + if (!gameObject.Valid) return collectionManager.Active.ByType(CollectionType.Yourself) ?? collectionManager.Active.Default; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 78250341..d3bc19b0 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -7,8 +7,8 @@ public interface IMod { LowerString Name { get; } - public int Index { get; } - public int Priority { get; } + public int Index { get; } + public ModPriority Priority { get; } public ISubMod Default { get; } public IReadOnlyList Groups { get; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 60508d33..ea6a62df 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,3 +1,4 @@ +using System.Security.AccessControl; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -33,6 +34,8 @@ public enum ModOptionChangeType public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) { + + /// Change the type of a group given by mod and index to type, if possible. public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { @@ -46,7 +49,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Change the settings stored as default options in a mod. - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, Setting defaultOption) { var group = mod.Groups[groupIdx]; if (group.DefaultSettings == defaultOption) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index c5e671af..b7d1186d 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -12,7 +12,7 @@ public sealed class Mod : IMod { Name = "Forced Files", Index = -1, - Priority = int.MaxValue, + Priority = ModPriority.MaxValue, }; // Main Data @@ -26,9 +26,9 @@ public sealed class Mod : IMod public bool IsTemporary => Index < 0; - /// Unused if Index < 0 but used for special temporary mods. - public int Priority - => 0; + /// Unused if Index is less than 0 but used for special temporary mods. + public ModPriority Priority + => ModPriority.Default; internal Mod(DirectoryInfo modPath) { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 042c98b4..c324af48 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, uint defaultSettings, string desc, IEnumerable subMods) + int priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index ea5f176c..2f6b2403 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -12,7 +12,7 @@ public interface IModGroup : IEnumerable public string Description { get; } public GroupType Type { get; } public int Priority { get; } - public uint DefaultSettings { get; set; } + public Setting DefaultSettings { get; set; } public int OptionPriority(Index optionIdx); @@ -31,6 +31,9 @@ public interface IModGroup : IEnumerable public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); public void UpdatePositions(int from = 0); + + /// Ensure that a value is valid for a group. + public Setting FixSetting(Setting setting); } public readonly struct ModSaveGroup : ISavable @@ -87,7 +90,7 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName(nameof(Type)); j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); - j.WriteValue(_group.DefaultSettings); + j.WriteValue(_group.DefaultSettings.Value); j.WritePropertyName("Options"); j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) diff --git a/Penumbra/Mods/Subclasses/ModPriority.cs b/Penumbra/Mods/Subclasses/ModPriority.cs new file mode 100644 index 00000000..3302c627 --- /dev/null +++ b/Penumbra/Mods/Subclasses/ModPriority.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; + +namespace Penumbra.Mods.Subclasses; + +[JsonConverter(typeof(Converter))] +public readonly record struct ModPriority(int Value) : + IComparisonOperators, + IAdditionOperators, + IAdditionOperators, + ISubtractionOperators, + ISubtractionOperators +{ + public static readonly ModPriority Default = new(0); + public static readonly ModPriority MaxValue = new(int.MaxValue); + + public bool IsDefault + => Value == Default.Value; + + public Setting AsSetting + => new((uint)Value); + + public ModPriority Max(ModPriority other) + => this < other ? other : this; + + public override string ToString() + => Value.ToString(); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ModPriority value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override ModPriority ReadJson(JsonReader reader, Type objectType, ModPriority existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } + + public static bool operator >(ModPriority left, ModPriority right) + => left.Value > right.Value; + + public static bool operator >=(ModPriority left, ModPriority right) + => left.Value >= right.Value; + + public static bool operator <(ModPriority left, ModPriority right) + => left.Value < right.Value; + + public static bool operator <=(ModPriority left, ModPriority right) + => left.Value <= right.Value; + + public static ModPriority operator +(ModPriority left, ModPriority right) + => new(left.Value + right.Value); + + public static ModPriority operator +(ModPriority left, int right) + => new(left.Value + right); + + public static ModPriority operator -(ModPriority left, ModPriority right) + => new(left.Value - right.Value); + + public static ModPriority operator -(ModPriority left, int right) + => new(left.Value - right); +} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index ed8ad84e..b79b3242 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -11,8 +11,8 @@ namespace Penumbra.Mods.Subclasses; public class ModSettings { public static readonly ModSettings Empty = new(); - public List Settings { get; private init; } = []; - public int Priority { get; set; } + public SettingList Settings { get; private init; } = []; + public ModPriority Priority { get; set; } public bool Enabled { get; set; } // Create an independent copy of the current settings. @@ -21,7 +21,7 @@ public class ModSettings { Enabled = Enabled, Priority = Priority, - Settings = [.. Settings], + Settings = Settings.Clone(), }; // Create default settings for a given mod. @@ -29,8 +29,8 @@ public class ModSettings => new() { Enabled = false, - Priority = 0, - Settings = mod.Groups.Select(g => g.DefaultSettings).ToList(), + Priority = ModPriority.Default, + Settings = SettingList.Default(mod), }; // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. @@ -39,7 +39,7 @@ public class ModSettings if (settings == null) settings = DefaultSettings(mod); else - settings.AddMissingSettings(mod); + settings.Settings.FixSize(mod); var dict = new Dictionary(); var set = new HashSet(); @@ -49,13 +49,13 @@ public class ModSettings if (group.Type is GroupType.Single) { if (group.Count > 0) - AddOption(group[(int)settings.Settings[index]]); + AddOption(group[settings.Settings[index].AsIndex]); } else { foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index))) { - if (((settings.Settings[index] >> optionIdx) & 1) == 1) + if (settings.Settings[index].HasFlag(optionIdx)) AddOption(option); } } @@ -97,8 +97,8 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => (uint)Math.Max(Math.Min(group.Count - 1, BitOperations.TrailingZeroCount(config)), 0), - GroupType.Multi => 1u << (int)config, + GroupType.Single => config.TurnMulti(group.Count), + GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; return config != Settings[groupIdx]; @@ -111,9 +111,11 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, - GroupType.Multi => Functions.RemoveBit(config, optionIdx), - _ => config, + GroupType.Single => config.AsIndex >= optionIdx + ? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero + : config, + GroupType.Multi => config.RemoveBit(optionIdx), + _ => config, }; return config != Settings[groupIdx]; } @@ -128,8 +130,8 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config == optionIdx ? (uint)movedToIdx : config, - GroupType.Multi => Functions.MoveBit(config, optionIdx, movedToIdx), + GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config, + GroupType.Multi => config.MoveBit(optionIdx, movedToIdx), _ => config, }; return config != Settings[groupIdx]; @@ -138,96 +140,52 @@ public class ModSettings } } - public bool FixAllSettings(Mod mod) + /// Set a setting. Ensures that there are enough settings and fixes the setting beforehand. + public void SetValue(Mod mod, int groupIdx, Setting newValue) { - var ret = false; - for (var i = 0; i < Settings.Count; ++i) - { - var newValue = FixSetting(mod.Groups[i], Settings[i]); - if (newValue != Settings[i]) - { - ret = true; - Settings[i] = newValue; - } - } - - return AddMissingSettings(mod) || ret; - } - - // Ensure that a value is valid for a group. - private static uint FixSetting(IModGroup group, uint value) - => group.Type switch - { - GroupType.Single => (uint)Math.Min(value, group.Count - 1), - GroupType.Multi => (uint)(value & ((1ul << group.Count) - 1)), - _ => value, - }; - - // Set a setting. Ensures that there are enough settings and fixes the setting beforehand. - public void SetValue(Mod mod, int groupIdx, uint newValue) - { - AddMissingSettings(mod); + Settings.FixSize(mod); var group = mod.Groups[groupIdx]; - Settings[groupIdx] = FixSetting(group, newValue); - } - - // Add defaulted settings up to the required count. - private bool AddMissingSettings(Mod mod) - { - var changes = false; - for (var i = Settings.Count; i < mod.Groups.Count; ++i) - { - Settings.Add(mod.Groups[i].DefaultSettings); - changes = true; - } - - return changes; + Settings[groupIdx] = group.FixSetting(newValue); } // A simple struct conversion to easily save settings by name instead of value. public struct SavedSettings { - public Dictionary Settings; - public int Priority; - public bool Enabled; + public Dictionary Settings; + public ModPriority Priority; + public bool Enabled; public SavedSettings DeepCopy() - => new() - { - Enabled = Enabled, - Priority = Priority, - Settings = Settings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), - }; + => this with { Settings = Settings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; public SavedSettings(ModSettings settings, Mod mod) { Priority = settings.Priority; Enabled = settings.Enabled; - Settings = new Dictionary(mod.Groups.Count); - settings.AddMissingSettings(mod); + Settings = new Dictionary(mod.Groups.Count); + settings.Settings.FixSize(mod); foreach (var (group, setting) in mod.Groups.Zip(settings.Settings)) Settings.Add(group.Name, setting); } // Convert and fix. - public bool ToSettings(Mod mod, out ModSettings settings) + public readonly bool ToSettings(Mod mod, out ModSettings settings) { - var list = new List(mod.Groups.Count); + var list = new SettingList(mod.Groups.Count); var changes = Settings.Count != mod.Groups.Count; foreach (var group in mod.Groups) { if (Settings.TryGetValue(group.Name, out var config)) { - var castConfig = (uint)Math.Clamp(config, 0, uint.MaxValue); - var actualConfig = FixSetting(group, castConfig); + var actualConfig = group.FixSetting(config); list.Add(actualConfig); if (actualConfig != config) changes = true; } else { - list.Add(0); + list.Add(group.DefaultSettings); changes = true; } } @@ -245,7 +203,7 @@ public class ModSettings // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. // Does not repair settings but ignores settings not fitting to the given mod. - public (bool Enabled, int Priority, Dictionary> Settings) ConvertToShareable(Mod mod) + public (bool Enabled, ModPriority Priority, Dictionary> Settings) ConvertToShareable(Mod mod) { var dict = new Dictionary>(Settings.Count); foreach (var (setting, idx) in Settings.WithIndex()) @@ -254,16 +212,13 @@ public class ModSettings break; var group = mod.Groups[idx]; - if (group.Type == GroupType.Single && setting < group.Count) + if (group.Type == GroupType.Single && setting.Value < (ulong)group.Count) { - dict.Add(group.Name, new[] - { - group[(int)setting].Name, - }); + dict.Add(group.Name, [group[(int)setting.Value].Name]); } else { - var list = group.Where((_, optionIdx) => (setting & (1 << optionIdx)) != 0).Select(o => o.Name).ToList(); + var list = group.Where((_, optionIdx) => (setting.Value & (1ul << optionIdx)) != 0).Select(o => o.Name).ToList(); dict.Add(group.Name, list); } } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 07f84722..8a8e10bd 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -14,10 +14,10 @@ public sealed class MultiModGroup : IModGroup public GroupType Type => GroupType.Multi; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } + public Setting DefaultSettings { get; set; } public int OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; @@ -44,7 +44,7 @@ public sealed class MultiModGroup : IModGroup Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? 0, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) return null; @@ -56,7 +56,8 @@ public sealed class MultiModGroup : IModGroup if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { Penumbra.Messager.NotificationMessage( - $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", NotificationType.Warning); + $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", + NotificationType.Warning); break; } @@ -66,7 +67,7 @@ public sealed class MultiModGroup : IModGroup ret.PrioritizedOptions.Add((subMod, priority)); } - ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } @@ -82,7 +83,7 @@ public sealed class MultiModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), + DefaultSettings = DefaultSettings.TurnMulti(Count), }; multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; @@ -95,7 +96,7 @@ public sealed class MultiModGroup : IModGroup if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) return false; - DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); + DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } @@ -105,4 +106,7 @@ public sealed class MultiModGroup : IModGroup foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) o.SetPosition(o.GroupIdx, i); } + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << Count) - 1)); } diff --git a/Penumbra/Mods/Subclasses/Setting.cs b/Penumbra/Mods/Subclasses/Setting.cs new file mode 100644 index 00000000..18b1e4ca --- /dev/null +++ b/Penumbra/Mods/Subclasses/Setting.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; +using OtterGui; + +namespace Penumbra.Mods.Subclasses; + +[JsonConverter(typeof(Converter))] +public readonly record struct Setting(ulong Value) +{ + public static readonly Setting Zero = new(0); + public static readonly Setting True = new(1); + public static readonly Setting False = new(0); + public static readonly Setting Indefinite = new(ulong.MaxValue); + + public static Setting Multi(int idx) + => new(1ul << idx); + + public static Setting Single(int idx) + => new(Math.Max(0ul, (ulong)idx)); + + public static Setting operator |(Setting lhs, Setting rhs) + => new(lhs.Value | rhs.Value); + + public int AsIndex + => (int)Math.Clamp(Value, 0ul, int.MaxValue); + + public bool HasFlag(int idx) + => idx >= 0 && (Value & (1ul << idx)) != 0; + + public Setting MoveBit(int idx1, int idx2) + => new(Functions.MoveBit(Value, idx1, idx2)); + + public Setting RemoveBit(int idx) + => new(Functions.RemoveBit(Value, idx)); + + public Setting SetBit(int idx, bool value) + => new(value ? Value | (1ul << idx) : Value & ~(1ul << idx)); + + public static Setting AllBits(int count) + => new((1ul << Math.Clamp(count, 0, 63)) - 1); + + public Setting TurnMulti(int count) + => new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0)); + + public ModPriority AsPriority + => new((int)(Value & 0xFFFFFFFF)); + + public static Setting FromBool(bool value) + => value ? True : False; + + public bool AsBool + => Value != 0; + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Setting value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override Setting ReadJson(JsonReader reader, Type objectType, Setting existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Mods/Subclasses/SettingList.cs b/Penumbra/Mods/Subclasses/SettingList.cs new file mode 100644 index 00000000..ea1e447f --- /dev/null +++ b/Penumbra/Mods/Subclasses/SettingList.cs @@ -0,0 +1,57 @@ +namespace Penumbra.Mods.Subclasses; + +public class SettingList : List +{ + public SettingList() + { } + + public SettingList(int capacity) + : base(capacity) + { } + + public SettingList(IEnumerable settings) + => AddRange(settings); + + public SettingList Clone() + => new(this); + + public static SettingList Default(Mod mod) + => new(mod.Groups.Select(g => g.DefaultSettings)); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool FixSize(Mod mod) + { + var diff = Count - mod.Groups.Count; + + switch (diff) + { + case 0: return false; + case > 0: + RemoveRange(mod.Groups.Count, diff); + return true; + default: + EnsureCapacity(mod.Groups.Count); + for (var i = Count; i < mod.Groups.Count; ++i) + Add(mod.Groups[i].DefaultSettings); + return true; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool FixAll(Mod mod) + { + var ret = false; + for (var i = 0; i < Count; ++i) + { + var oldValue = this[i]; + var newValue = mod.Groups[i].FixSetting(oldValue); + if (newValue == oldValue) + continue; + + ret = true; + this[i] = newValue; + } + + return FixSize(mod) | ret; + } +} diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 2b7ebd09..be1dbde5 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -12,10 +12,10 @@ public sealed class SingleModGroup : IModGroup public GroupType Type => GroupType.Single; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public int Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -43,7 +43,7 @@ public sealed class SingleModGroup : IModGroup Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? 0, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0u, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) return null; @@ -57,9 +57,7 @@ public sealed class SingleModGroup : IModGroup ret.OptionData.Add(subMod); } - if ((int)ret.DefaultSettings >= ret.Count) - ret.DefaultSettings = 0; - + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } @@ -74,7 +72,7 @@ public sealed class SingleModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = 1u << (int)DefaultSettings, + DefaultSettings = Setting.Multi((int) DefaultSettings.Value), }; multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, i))); return multi; @@ -87,19 +85,20 @@ public sealed class SingleModGroup : IModGroup if (!OptionData.Move(optionIdxFrom, optionIdxTo)) return false; + var currentIndex = DefaultSettings.AsIndex; // Update default settings with the move. - if (DefaultSettings == optionIdxFrom) + if (currentIndex == optionIdxFrom) { - DefaultSettings = (uint)optionIdxTo; + DefaultSettings = Setting.Single(optionIdxTo); } else if (optionIdxFrom < optionIdxTo) { - if (DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo) - --DefaultSettings; + if (currentIndex > optionIdxFrom && currentIndex <= optionIdxTo) + DefaultSettings = Setting.Single(currentIndex - 1); } - else if (DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo) + else if (currentIndex < optionIdxFrom && currentIndex >= optionIdxTo) { - ++DefaultSettings; + DefaultSettings = Setting.Single(currentIndex + 1); } UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); @@ -111,4 +110,7 @@ public sealed class SingleModGroup : IModGroup foreach (var (o, i) in OptionData.WithIndex().Skip(from)) o.SetPosition(o.GroupIdx, i); } + + public Setting FixSetting(Setting setting) + => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index c80334aa..4de2ac13 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -13,7 +13,7 @@ public class TemporaryMod : IMod { public LowerString Name { get; init; } = LowerString.Empty; public int Index { get; init; } = -2; - public int Priority { get; init; } = int.MaxValue; + public ModPriority Priority { get; init; } = ModPriority.MaxValue; public int TotalManipulations => Default.Manipulations.Count; @@ -27,10 +27,7 @@ public class TemporaryMod : IMod => Array.Empty(); public IEnumerable AllSubMods - => new[] - { - Default, - }; + => [Default]; public TemporaryMod() => Default = new SubMod(this); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index b84c0996..d1e952f1 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -341,15 +341,15 @@ public class ConfigMigrationService(SaveService saveService) : IService var text = File.ReadAllText(collectionJson.FullName); var data = JArray.Parse(text); - var maxPriority = 0; + var maxPriority = ModPriority.Default; var dict = new Dictionary(); foreach (var setting in data.Cast()) { - var modName = (string)setting["FolderName"]!; - var enabled = (bool)setting["Enabled"]!; - var priority = (int)setting["Priority"]!; - var settings = setting["Settings"]!.ToObject>() - ?? setting["Conf"]!.ToObject>(); + var modName = setting["FolderName"]?.ToObject()!; + var enabled = setting["Enabled"]?.ToObject() ?? false; + var priority = setting["Priority"]?.ToObject() ?? ModPriority.Default; + var settings = setting["Settings"]!.ToObject>() + ?? setting["Conf"]!.ToObject>(); dict[modName] = new ModSettings.SavedSettings() { @@ -357,7 +357,7 @@ public class ConfigMigrationService(SaveService saveService) : IService Priority = priority, Settings = settings!, }; - maxPriority = Math.Max(maxPriority, priority); + maxPriority = maxPriority.Max(priority); } InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; @@ -365,8 +365,7 @@ public class ConfigMigrationService(SaveService saveService) : IService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, - Array.Empty()); + var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 0205f3c6..7b5ce2dc 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -694,7 +694,7 @@ public class ItemSwapTab : IDisposable, ITab UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); } - private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) + private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) { if (collection != _collectionManager.Active.Current || mod != _mod) return; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 15b18692..cd0eb982 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -74,7 +74,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { var mod = _modManager.FirstOrDefault(m @@ -92,7 +92,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Label => "Conflicts"u8; public bool IsVisible - => _collectionManager.Active.Current.Conflicts(_selector.Selected!).Count > 0; + => collectionManager.Active.Current.Conflicts(selector.Selected!).Count > 0; - private readonly ConditionalWeakTable _expandedMods = new(); + private readonly ConditionalWeakTable _expandedMods = []; - private int GetPriority(ModConflicts conflicts) + private ModPriority GetPriority(ModConflicts conflicts) { if (conflicts.Mod2.Index < 0) return conflicts.Mod2.Priority; - return _collectionManager.Active.Current[conflicts.Mod2.Index].Settings?.Priority ?? 0; + return collectionManager.Active.Current[conflicts.Mod2.Index].Settings?.Priority ?? ModPriority.Default; } public void DrawContent() @@ -63,8 +55,8 @@ public class ModPanelConflictsTab : ITab DrawCurrentRow(priorityWidth); // Can not be null because otherwise the tab bar is never drawn. - var mod = _selector.Selected!; - foreach (var (conflict, index) in _collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) + var mod = selector.Selected!; + foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) .ThenBy(c => c.Mod2.Name.Lower).WithIndex()) { using var id = ImRaii.PushId(index); @@ -77,18 +69,18 @@ public class ModPanelConflictsTab : ITab ImGui.TableNextColumn(); using var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderLine.Value()); ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_selector.Selected!.Name); + ImGui.TextUnformatted(selector.Selected!.Name); ImGui.TableNextColumn(); - var priority = _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority; + var priority = collectionManager.Active.Current[selector.Selected!.Index].Settings!.Priority.Value; ImGui.SetNextItemWidth(priorityWidth); if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, (Mod)_selector.Selected!, - _currentPriority.Value); + if (_currentPriority != collectionManager.Active.Current[selector.Selected!.Index].Settings!.Priority.Value) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -104,7 +96,7 @@ public class ModPanelConflictsTab : ITab { ImGui.AlignTextToFramePadding(); if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) - _selector.SelectByValue(otherMod); + selector.SelectByValue(otherMod); var hovered = ImGui.IsItemHovered(); var rightClicked = ImGui.IsItemClicked(ImGuiMouseButton.Right); if (conflict.Mod2 is Mod otherMod2) @@ -112,7 +104,7 @@ public class ModPanelConflictsTab : ITab if (hovered) ImGui.SetTooltip("Click to jump to mod, Control + Right-Click to disable mod."); if (rightClicked && ImGui.GetIO().KeyCtrl) - _collectionManager.Editor.SetModState(_collectionManager.Active.Current, otherMod2, false); + collectionManager.Editor.SetModState(collectionManager.Active.Current, otherMod2, false); } } @@ -146,7 +138,7 @@ public class ModPanelConflictsTab : ITab ImGui.TableNextColumn(); var conflictPriority = DrawPriorityInput(conflict, priorityWidth); ImGui.SameLine(); - var selectedPriority = _collectionManager.Active.Current[_selector.Selected!.Index].Settings!.Priority; + var selectedPriority = collectionManager.Active.Current[selector.Selected!.Index].Settings!.Priority.Value; DrawPriorityButtons(conflict.Mod2 as Mod, conflictPriority, selectedPriority, buttonSize); ImGui.TableNextColumn(); DrawExpandButton(conflict.Mod2, expanded, buttonSize); @@ -171,7 +163,7 @@ public class ModPanelConflictsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Text, conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value()); using var disabled = ImRaii.Disabled(conflict.Mod2.Index < 0); - var priority = _currentPriority ?? GetPriority(conflict); + var priority = _currentPriority ?? GetPriority(conflict).Value; ImGui.SetNextItemWidth(priorityWidth); if (ImGui.InputInt("##priority", ref priority, 0, 0, ImGuiInputTextFlags.EnterReturnsTrue)) @@ -179,8 +171,9 @@ public class ModPanelConflictsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != GetPriority(conflict)) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, (Mod)conflict.Mod2, _currentPriority.Value); + if (_currentPriority != GetPriority(conflict).Value) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, (Mod)conflict.Mod2, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -195,12 +188,14 @@ public class ModPanelConflictsTab : ITab private void DrawPriorityButtons(Mod? conflict, int conflictPriority, int selectedPriority, Vector2 buttonSize) { if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SortNumericUpAlt.ToIconString(), buttonSize, - $"Set the priority of the currently selected mod to this mods priority plus one. ({selectedPriority} -> {conflictPriority + 1})", selectedPriority > conflictPriority, true)) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, conflictPriority + 1); + $"Set the priority of the currently selected mod to this mods priority plus one. ({selectedPriority} -> {conflictPriority + 1})", + selectedPriority > conflictPriority, true)) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(conflictPriority + 1)); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.SortNumericDownAlt.ToIconString(), buttonSize, $"Set the priority of this mod to the currently selected mods priority minus one. ({conflictPriority} -> {selectedPriority - 1})", selectedPriority > conflictPriority || conflict == null, true)) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, conflict!, selectedPriority - 1); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, conflict!, new ModPriority(selectedPriority - 1)); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index eb79869e..1292367a 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -502,18 +502,16 @@ public class ModPanelEditTab( if (group.Type == GroupType.Single) { - if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); + if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, Setting.Single(optionIdx)); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } else { - var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption - ? group.DefaultSettings | (1u << optionIdx) - : group.DefaultSettings & ~(1u << optionIdx)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index b14cad01..1107aa20 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -26,7 +26,7 @@ public class ModPanelSettingsTab : ITab private ModSettings _settings = null!; private ModCollection _collection = null!; private bool _empty; - private int? _currentPriority = null; + private int? _currentPriority; public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, TutorialService tutorial, CommunicatorService communicator, Configuration config) @@ -136,15 +136,15 @@ public class ModPanelSettingsTab : ITab private void DrawPriorityInput() { using var group = ImRaii.Group(); - var priority = _currentPriority ?? _settings.Priority; + var priority = _currentPriority ?? _settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); if (ImGui.InputInt("##Priority", ref priority, 0, 0)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != _settings.Priority) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, _currentPriority.Value); + if (_currentPriority != _settings.Priority.Value) + _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -179,7 +179,7 @@ public class ModPanelSettingsTab : ITab private void DrawSingleGroupCombo(IModGroup group, int groupIdx) { using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; + var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) { @@ -189,7 +189,8 @@ public class ModPanelSettingsTab : ITab id.Push(idx2); var option = group[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx2); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, + Setting.Single(idx2)); if (option.Description.Length > 0) ImGuiUtil.SelectableHelpMarker(option.Description); @@ -210,8 +211,8 @@ public class ModPanelSettingsTab : ITab private void DrawSingleGroupRadio(IModGroup group, int groupIdx) { using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, description:group.Description); + var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); DrawCollapseHandling(group, minWidth, DrawOptions); @@ -225,7 +226,8 @@ public class ModPanelSettingsTab : ITab using var i = ImRaii.PushId(idx); var option = group[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, (uint)idx); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, + Setting.Single(idx)); if (option.Description.Length <= 0) continue; @@ -291,7 +293,17 @@ public class ModPanelSettingsTab : ITab { using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, description: group.Description); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + + DrawCollapseHandling(group, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + DrawMultiPopup(group, groupIdx, label); + return; void DrawOptions() { @@ -299,12 +311,11 @@ public class ModPanelSettingsTab : ITab { using var i = ImRaii.PushId(idx); var option = group[idx]; - var flag = 1u << idx; - var setting = (flags & flag) != 0; + var setting = flags.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref setting)) { - flags = setting ? flags | flag : flags & ~flag; + flags = flags.SetBit(idx, setting); _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); } @@ -315,14 +326,10 @@ public class ModPanelSettingsTab : ITab } } } + } - DrawCollapseHandling(group, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); - + private void DrawMultiPopup(IModGroup group, int groupIdx, string label) + { using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); using var popup = ImRaii.Popup(label); if (!popup) @@ -331,12 +338,10 @@ public class ModPanelSettingsTab : ITab ImGui.TextUnformatted(group.Name); ImGui.Separator(); if (ImGui.Selectable("Enable All")) - { - flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); - } + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, + Setting.AllBits(group.Count)); if (ImGui.Selectable("Disable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, 0); + _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 06f1d126..9a956d2d 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -211,7 +211,7 @@ public class DebugTab : Window, ITab color.Pop(); foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) { - using var id = mod is TemporaryMod t ? PushId(t.Priority) : PushId(((Mod)mod).ModPath.Name); + using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name); using var node2 = TreeNode(mod.Name.Text); if (!node2) continue; From e94cdaec4627cf06e4dfcb5cb55e746779a04f94 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Apr 2024 23:19:41 +0200 Subject: [PATCH 018/865] Some more. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 6 +++--- Penumbra/Mods/Manager/ModMigration.cs | 12 ++++++------ Penumbra/Mods/Manager/ModOptionEditor.cs | 18 ++++++++++-------- Penumbra/Mods/ModCreator.cs | 4 ++-- Penumbra/Mods/Subclasses/IModGroup.cs | 18 +++++++++++------- Penumbra/Mods/Subclasses/ISubMod.cs | 4 ++-- Penumbra/Mods/Subclasses/ModPriority.cs | 10 +++++++++- Penumbra/Mods/Subclasses/MultiModGroup.cs | 14 +++++++------- Penumbra/Mods/Subclasses/SingleModGroup.cs | 16 ++++++++-------- Penumbra/Mods/Subclasses/SubMod.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 19 ++++++++++--------- 11 files changed, 70 insertions(+), 55 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7a247a53..099b133c 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -153,7 +153,7 @@ public partial class TexToolsImporter // Iterate through all pages var options = new List(); - var groupPriority = 0; + var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) { @@ -209,9 +209,9 @@ public partial class TexToolsImporter } } - _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + _modManager.Creator.CreateOptionGroup(_currentModDirectory, group.SelectionType, name, groupPriority, groupPriority.Value, defaultSettings ?? Setting.Zero, group.Description, options); - ++groupPriority; + groupPriority += 1; } } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 8b73cae5..295afd7b 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -61,10 +61,9 @@ public static partial class ModMigration if (fileVersion > 0) return false; - var swaps = json["FileSwaps"]?.ToObject>() - ?? new Dictionary(); - var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); - var priority = 1; + var swaps = json["FileSwaps"]?.ToObject>() ?? []; + var groups = json["Groups"]?.ToObject>() ?? []; + var priority = new ModPriority(1); var seenMetaFiles = new HashSet(); foreach (var group in groups.Values) ConvertGroup(creator, mod, group, ref priority, seenMetaFiles); @@ -116,7 +115,8 @@ public static partial class ModMigration return true; } - private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) + private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref ModPriority priority, + HashSet seenMetaFiles) { if (group.Options.Count == 0) return; @@ -125,7 +125,7 @@ public static partial class ModMigration { case GroupType.Multi: - var optionPriority = 0; + var optionPriority = ModPriority.Default; var newMultiGroup = new MultiModGroup() { Name = group.GroupName, diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index ea6a62df..0ffdc4af 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -34,8 +34,6 @@ public enum ModOptionChangeType public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) { - - /// Change the type of a group given by mod and index to type, if possible. public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { @@ -86,7 +84,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!VerifyFileName(mod, null, newName, true)) return; - var maxPriority = mod.Groups.Count == 0 ? 0 : mod.Groups.Max(o => o.Priority) + 1; + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; mod.Groups.Add(type == GroupType.Multi ? new MultiModGroup @@ -169,7 +167,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Change the internal priority of the given option group. - public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) + public void ChangeGroupPriority(Mod mod, int groupIdx, ModPriority newPriority) { var group = mod.Groups[groupIdx]; if (group.Priority == newPriority) @@ -186,7 +184,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Change the internal priority of the given option. - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, ModPriority newPriority) { switch (mod.Groups[groupIdx]) { @@ -240,7 +238,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS s.OptionData.Add(subMod); break; case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, 0)); + m.PrioritizedOptions.Add((subMod, ModPriority.Default)); break; } @@ -263,8 +261,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return ((SubMod)group[^1], true); } - /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) + /// Add an existing option to a given group with default priority. + public void AddOption(Mod mod, int groupIdx, ISubMod option) + => AddOption(mod, groupIdx, option, ModPriority.Default); + + /// Add an existing option to a given group with a given priority. + public void AddOption(Mod mod, int groupIdx, ISubMod option, ModPriority priority) { if (option is not SubMod o) return; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index c324af48..2bcdd3b1 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, idx))); + group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 2f6b2403..e9e2a93b 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -8,13 +8,13 @@ public interface IModGroup : IEnumerable { public const int MaxMultiOptions = 32; - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public int Priority { get; } - public Setting DefaultSettings { get; set; } + public string Name { get; } + public string Description { get; } + public GroupType Type { get; } + public ModPriority Priority { get; } + public Setting DefaultSettings { get; set; } - public int OptionPriority(Index optionIdx); + public ModPriority OptionPriority(Index optionIdx); public ISubMod this[Index idx] { get; } @@ -94,7 +94,11 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName("Options"); j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) - ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type == GroupType.Multi ? _group.OptionPriority(idx) : null); + ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch + { + GroupType.Multi => _group.OptionPriority(idx), + _ => null, + }); j.WriteEndArray(); j.WriteEndObject(); diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 8c296f20..29323c1d 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -16,7 +16,7 @@ public interface ISubMod public bool IsDefault { get; } - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, int? priority) + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) { j.WriteStartObject(); j.WritePropertyName(nameof(Name)); @@ -26,7 +26,7 @@ public interface ISubMod if (priority != null) { j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value); + j.WriteValue(priority.Value.Value); } j.WritePropertyName(nameof(mod.Files)); diff --git a/Penumbra/Mods/Subclasses/ModPriority.cs b/Penumbra/Mods/Subclasses/ModPriority.cs index 3302c627..a99c12ed 100644 --- a/Penumbra/Mods/Subclasses/ModPriority.cs +++ b/Penumbra/Mods/Subclasses/ModPriority.cs @@ -8,7 +8,9 @@ public readonly record struct ModPriority(int Value) : IAdditionOperators, IAdditionOperators, ISubtractionOperators, - ISubtractionOperators + ISubtractionOperators, + IIncrementOperators, + IComparable { public static readonly ModPriority Default = new(0); public static readonly ModPriority MaxValue = new(int.MaxValue); @@ -58,4 +60,10 @@ public readonly record struct ModPriority(int Value) : public static ModPriority operator -(ModPriority left, int right) => new(left.Value - right); + + public static ModPriority operator ++(ModPriority value) + => new(value.Value + 1); + + public int CompareTo(ModPriority other) + => Value.CompareTo(other.Value); } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 8a8e10bd..444e8e2c 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -14,12 +14,12 @@ public sealed class MultiModGroup : IModGroup public GroupType Type => GroupType.Multi; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public Setting DefaultSettings { get; set; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } - public int OptionPriority(Index idx) + public ModPriority OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; public ISubMod this[Index idx] @@ -29,7 +29,7 @@ public sealed class MultiModGroup : IModGroup public int Count => PrioritizedOptions.Count; - public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); + public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; public IEnumerator GetEnumerator() => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); @@ -43,7 +43,7 @@ public sealed class MultiModGroup : IModGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? 0, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index be1dbde5..0bfa04f4 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -12,14 +12,14 @@ public sealed class SingleModGroup : IModGroup public GroupType Type => GroupType.Single; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } - public Setting DefaultSettings { get; set; } + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; - public int OptionPriority(Index _) + public ModPriority OptionPriority(Index _) => Priority; public ISubMod this[Index idx] @@ -42,7 +42,7 @@ public sealed class SingleModGroup : IModGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? 0, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -72,9 +72,9 @@ public sealed class SingleModGroup : IModGroup Name = Name, Description = Description, Priority = Priority, - DefaultSettings = Setting.Multi((int) DefaultSettings.Value), + DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; - multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, i))); + multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, new ModPriority(i)))); return multi; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 88c4e4ce..4f35cd33 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -53,7 +53,7 @@ public sealed class SubMod : ISubMod OptionIdx = optionIdx; } - public void Load(DirectoryInfo basePath, JToken json, out int priority) + public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) { FileData.Clear(); FileSwapData.Clear(); @@ -62,7 +62,7 @@ public sealed class SubMod : ISubMod // Every option has a name, but priorities are only relevant for multi group options. Name = json[nameof(ISubMod.Name)]?.ToObject() ?? string.Empty; Description = json[nameof(ISubMod.Description)]?.ToObject() ?? string.Empty; - priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? 0; + priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; var files = (JObject?)json[nameof(Files)]; if (files != null) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1292367a..80af7b15 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -511,7 +511,8 @@ public class ModPanelEditTab( { var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, + group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } @@ -669,10 +670,10 @@ public class ModPanelEditTab( public const int Description = -7; // Temporary strings - private static string? _currentEdit; - private static int? _currentGroupPriority; - private static int _currentField = None; - private static int _optionIndex = None; + private static string? _currentEdit; + private static ModPriority? _currentGroupPriority; + private static int _currentField = None; + private static int _optionIndex = None; public static void Reset() { @@ -705,13 +706,13 @@ public class ModPanelEditTab( return false; } - public static bool Priority(string label, int field, int option, int oldValue, out int value, float width) + public static bool Priority(string label, int field, int option, ModPriority oldValue, out ModPriority value, float width) { - var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; + var tmp = (field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue).Value; ImGui.SetNextItemWidth(width); if (ImGui.InputInt(label, ref tmp, 0, 0)) { - _currentGroupPriority = tmp; + _currentGroupPriority = new ModPriority(tmp); _optionIndex = option; _currentField = field; } @@ -724,7 +725,7 @@ public class ModPanelEditTab( return ret; } - value = 0; + value = ModPriority.Default; return false; } } From 21a55b95d9a3379e3df1c8cd885780412476fc1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Apr 2024 15:19:44 +0200 Subject: [PATCH 019/865] Add rename mod field. --- Penumbra/Configuration.cs | 24 +++++----- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 49 +++++++++++++++++++- Penumbra/UI/ModsTab/RenameField.cs | 26 +++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 30 ++++++++++++ 4 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 Penumbra/UI/ModsTab/RenameField.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f91e0534..98668e8a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,6 +11,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -40,17 +41,18 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index cd0eb982..11a2d96f 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -66,7 +66,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector ClearQuickMove(1, _config.QuickMoveFolder2, () => {_config.QuickMoveFolder2 = string.Empty; _config.Save();}), 120); SubscribeRightClickMain(() => ClearQuickMove(2, _config.QuickMoveFolder3, () => {_config.QuickMoveFolder3 = string.Empty; _config.Save();}), 130); UnsubscribeRightClickLeaf(RenameLeaf); - SubscribeRightClickLeaf(RenameLeafMod, 1000); + SetRenameSearchPath(_config.ShowRename); AddButton(AddNewModButton, 0); AddButton(AddImportModButton, 1); AddButton(AddHelpButton, 2); @@ -92,6 +92,37 @@ public sealed class ModFileSystemSelector : FileSystemSelector DeleteSelectionButton(size, _config.DeleteModModifier, "mod", "mods", _modManager.DeleteMod); diff --git a/Penumbra/UI/ModsTab/RenameField.cs b/Penumbra/UI/ModsTab/RenameField.cs new file mode 100644 index 00000000..00232750 --- /dev/null +++ b/Penumbra/UI/ModsTab/RenameField.cs @@ -0,0 +1,26 @@ +namespace Penumbra.UI.ModsTab; + +public enum RenameField +{ + None, + RenameSearchPath, + RenameData, + BothSearchPathPrio, + BothDataPrio, +} + +public static class RenameFieldExtensions +{ + public static (string Name, string Desc) GetData(this RenameField value) + => value switch + { + RenameField.None => ("None", "Show no rename fields in the context menu for mods."), + RenameField.RenameSearchPath => ("Search Path", "Show only the search path / move field in the context menu for mods."), + RenameField.RenameData => ("Mod Name", "Show only the mod name field in the context menu for mods."), + RenameField.BothSearchPathPrio => ("Both (Focus Search Path)", + "Show both rename fields in the context menu for mods, but put the keyboard cursor on the search path field."), + RenameField.BothDataPrio => ("Both (Focus Mod Name)", + "Show both rename fields in the context menu for mods, but put the keyboard cursor on the mod name field"), + _ => (string.Empty, string.Empty), + }; +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c524a840..439f7be4 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -533,12 +533,42 @@ public class SettingsTab : ITab "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window."); } + private void DrawRenameSettings() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + using (var combo = ImRaii.Combo("##renameSettings", _config.ShowRename.GetData().Name)) + { + if (combo) + foreach (var value in Enum.GetValues()) + { + var (name, desc) = value.GetData(); + if (ImGui.Selectable(name, _config.ShowRename == value)) + { + _config.ShowRename = value; + _selector.SetRenameSearchPath(value); + _config.Save(); + } + + ImGuiUtil.HoverTooltip(desc); + } + } + + ImGui.SameLine(); + const string tt = + "Select which of the two renaming input fields are visible when opening the right-click context menu of a mod in the mod selector."; + ImGuiComponents.HelpMarker(tt); + ImGui.SameLine(); + ImGui.TextUnformatted("Rename Fields in Mod Context Menu"); + ImGuiUtil.HoverTooltip(tt); + } + /// Draw all settings pertaining to the mod selector. private void DrawModSelectorSettings() { DrawFolderSortType(); DrawAbsoluteSizeSelector(); DrawRelativeSizeSelector(); + DrawRenameSettings(); Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", _config.OpenFoldersByDefault, v => { From 7280c4b2f72c6b14b2a04b0387a1e6536429f7b9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Apr 2024 15:20:38 +0200 Subject: [PATCH 020/865] Selector improvements. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index f48c6886..5de71c22 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f48c6886cbc163c5a292fa8b9fd919cb01c11d7b +Subproject commit 5de71c22c03581738c25aa43d7dff10365ec7db3 From c0ee80629df66b987b391afb4625b742dc7a298c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Apr 2024 16:05:55 +0200 Subject: [PATCH 021/865] Allow right click to paste into filter as long as it is unfocused. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 5de71c22..4673e93f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5de71c22c03581738c25aa43d7dff10365ec7db3 +Subproject commit 4673e93f5165108a7f5b91236406d527f16384a5 From 45b1c55b67bc41fbf16aa36692c5d2657c380134 Mon Sep 17 00:00:00 2001 From: ocealot Date: Thu, 11 Apr 2024 15:00:54 -0400 Subject: [PATCH 022/865] Add accessory vfxs --- .../Hooks/Resources/ResolvePathHooksBase.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 6b4abf90..537992e2 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using OtterGui.Services; @@ -57,6 +58,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx); + _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], type, ResolveVfx, ResolveVfxHuman); // @formatter:on Enable(); } @@ -179,6 +181,27 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } + private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) + { + if (slotIndex <= 4) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + // Enable vfxs for accessories + var data = Marshal.ReadIntPtr(drawObject + 0xA38); + if (data == IntPtr.Zero) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + var slot = data + 12 * (nint)slotIndex; + var model = Marshal.ReadInt16(slot); + var variant = Marshal.ReadInt16(slot + 2); + var vfxId = Marshal.ReadInt16(slot + 8); + + if (model == 0 || variant == 0 || vfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + var path = "chara/accessory/a" + model.ToString().PadLeft(4, '0') + "/vfx/eff/va" + vfxId.ToString().PadLeft(4, '0') + ".avfx"; + + MemoryHelper.WriteString(pathBuffer, path); + Marshal.WriteIntPtr(unkOutParam, 0x00000004); + return ResolvePath(drawObject, pathBuffer); + } + private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) { data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); From eb0e7e2f5fd8ec434ff6caf0b047da8c8b5ee0eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 Apr 2024 21:25:00 +0200 Subject: [PATCH 023/865] Update ocealots code #1. --- .../Hooks/Resources/ResolvePathHooksBase.cs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 537992e2..f322267b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,5 +1,5 @@ +using System.Text.Unicode; using Dalamud.Hooking; -using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using OtterGui.Services; @@ -57,7 +57,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman); _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); - _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], ResolveVfx); _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], type, ResolveVfx, ResolveVfxHuman); // @formatter:on Enable(); @@ -183,22 +182,27 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex <= 4) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (slotIndex <= 4) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + var changedEquipData = ((Human*)drawObject)->ChangedEquipData; // Enable vfxs for accessories - var data = Marshal.ReadIntPtr(drawObject + 0xA38); - if (data == IntPtr.Zero) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var slot = data + 12 * (nint)slotIndex; - var model = Marshal.ReadInt16(slot); - var variant = Marshal.ReadInt16(slot + 2); - var vfxId = Marshal.ReadInt16(slot + 8); + var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex); + var model = slot[0]; + var variant = slot[1]; + var vfxId = slot[4]; - if (model == 0 || variant == 0 || vfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var path = "chara/accessory/a" + model.ToString().PadLeft(4, '0') + "/vfx/eff/va" + vfxId.ToString().PadLeft(4, '0') + ".avfx"; + if (model == 0 || variant == 0 || vfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - MemoryHelper.WriteString(pathBuffer, path); - Marshal.WriteIntPtr(unkOutParam, 0x00000004); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + *(ulong*)unkOutParam = 4; return ResolvePath(drawObject, pathBuffer); } From 793ed4f0a722c78b11332b391f8beeed938c896b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 00:02:09 +0200 Subject: [PATCH 024/865] With explicit null-termination, maybe? --- Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index f322267b..4e24ba39 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -198,7 +198,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable if (model == 0 || variant == 0 || vfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx", + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx\0", out _)) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); From ba8999914fd24408fb0635cffbb4da48b0dafc44 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 12:33:57 +0200 Subject: [PATCH 025/865] Rework API, use Collection ID in crash handler, use collection GUIDs in more places. --- OtterGui | 2 +- Penumbra.Api | 2 +- .../Buffers/AnimationInvocationBuffer.cs | 32 +- .../Buffers/CharacterBaseBuffer.cs | 48 +- .../Buffers/ModdedFileBuffer.cs | 60 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/ApiHelpers.cs | 76 + Penumbra/Api/Api/CollectionApi.cs | 141 ++ Penumbra/Api/Api/EditingApi.cs | 40 + Penumbra/Api/Api/GameStateApi.cs | 82 + Penumbra/Api/Api/MetaApi.cs | 23 + Penumbra/Api/Api/ModSettingsApi.cs | 282 +++ Penumbra/Api/Api/ModsApi.cs | 132 ++ Penumbra/Api/Api/PenumbraApi.cs | 40 + Penumbra/Api/Api/PluginStateApi.cs | 30 + Penumbra/Api/Api/RedrawApi.cs | 27 + Penumbra/Api/Api/ResolveApi.cs | 101 + Penumbra/Api/Api/ResourceTreeApi.cs | 63 + Penumbra/Api/Api/TemporaryApi.cs | 190 ++ Penumbra/Api/Api/UiApi.cs | 101 + Penumbra/Api/DalamudSubstitutionProvider.cs | 3 +- Penumbra/Api/HttpApi.cs | 22 +- Penumbra/Api/IpcProviders.cs | 118 ++ Penumbra/Api/IpcTester.cs | 1762 ----------------- .../Api/IpcTester/CollectionsIpcTester.cs | 166 ++ Penumbra/Api/IpcTester/EditingIpcTester.cs | 70 + Penumbra/Api/IpcTester/GameStateIpcTester.cs | 137 ++ Penumbra/Api/IpcTester/IpcTester.cs | 133 ++ Penumbra/Api/IpcTester/MetaIpcTester.cs | 38 + .../Api/IpcTester/ModSettingsIpcTester.cs | 181 ++ Penumbra/Api/IpcTester/ModsIpcTester.cs | 154 ++ .../Api/IpcTester/PluginStateIpcTester.cs | 132 ++ Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 72 + Penumbra/Api/IpcTester/ResolveIpcTester.cs | 114 ++ .../Api/IpcTester/ResourceTreeIpcTester.cs | 349 ++++ Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 203 ++ Penumbra/Api/IpcTester/UiIpcTester.cs | 128 ++ Penumbra/Api/PenumbraApi.cs | 1374 ------------- Penumbra/Api/PenumbraIpcProviders.cs | 435 ---- Penumbra/Collections/Cache/CollectionCache.cs | 10 +- .../Cache/CollectionCacheManager.cs | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 160 +- .../Collections/Manager/CollectionStorage.cs | 107 +- .../Manager/IndividualCollections.Files.cs | 98 +- .../Collections/Manager/InheritanceManager.cs | 11 +- .../Manager/TempCollectionManager.cs | 53 +- Penumbra/Collections/ModCollection.cs | 35 +- Penumbra/Collections/ModCollectionSave.cs | 18 +- Penumbra/CommandHandler.cs | 2 +- Penumbra/Communication/ChangedItemClick.cs | 3 +- Penumbra/Communication/ChangedItemHover.cs | 3 +- .../Communication/CreatedCharacterBase.cs | 1 + .../Communication/CreatingCharacterBase.cs | 3 +- Penumbra/Communication/EnabledChanged.cs | 3 +- Penumbra/Communication/ModDirectoryChanged.cs | 1 + Penumbra/Communication/ModFileChanged.cs | 1 + Penumbra/Communication/ModOptionChanged.cs | 1 + Penumbra/Communication/ModPathChanged.cs | 8 +- Penumbra/Communication/ModSettingChanged.cs | 1 + Penumbra/Communication/PostEnabledDraw.cs | 3 +- .../Communication/PostSettingsPanelDraw.cs | 3 +- .../Communication/PreSettingsPanelDraw.cs | 3 +- .../Communication/PreSettingsTabBarDraw.cs | 3 +- Penumbra/GuidExtensions.cs | 254 +++ Penumbra/Interop/PathResolving/MetaState.cs | 2 +- .../Interop/PathResolving/PathResolver.cs | 10 +- .../Interop/PathResolving/SubfileHelper.cs | 2 +- .../ResourceTree/ResourceTreeApiHelper.cs | 82 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 17 +- Penumbra/Mods/Manager/ModStorage.cs | 2 +- Penumbra/Mods/Subclasses/IModGroup.cs | 18 +- Penumbra/Mods/Subclasses/ModSettings.cs | 8 +- Penumbra/Mods/Subclasses/MultiModGroup.cs | 3 + Penumbra/Mods/Subclasses/SingleModGroup.cs | 3 + Penumbra/Mods/TemporaryMod.cs | 6 +- Penumbra/Penumbra.cs | 4 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/Services/ConfigMigrationService.cs | 4 +- Penumbra/Services/CrashHandlerService.cs | 6 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/Services/StaticServiceManager.cs | 10 +- .../ModEditWindow.QuickImport.cs | 4 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 54 +- .../UI/CollectionTab/CollectionSelector.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 4 +- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 6 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 18 +- 88 files changed, 4193 insertions(+), 3930 deletions(-) create mode 100644 Penumbra/Api/Api/ApiHelpers.cs create mode 100644 Penumbra/Api/Api/CollectionApi.cs create mode 100644 Penumbra/Api/Api/EditingApi.cs create mode 100644 Penumbra/Api/Api/GameStateApi.cs create mode 100644 Penumbra/Api/Api/MetaApi.cs create mode 100644 Penumbra/Api/Api/ModSettingsApi.cs create mode 100644 Penumbra/Api/Api/ModsApi.cs create mode 100644 Penumbra/Api/Api/PenumbraApi.cs create mode 100644 Penumbra/Api/Api/PluginStateApi.cs create mode 100644 Penumbra/Api/Api/RedrawApi.cs create mode 100644 Penumbra/Api/Api/ResolveApi.cs create mode 100644 Penumbra/Api/Api/ResourceTreeApi.cs create mode 100644 Penumbra/Api/Api/TemporaryApi.cs create mode 100644 Penumbra/Api/Api/UiApi.cs create mode 100644 Penumbra/Api/IpcProviders.cs delete mode 100644 Penumbra/Api/IpcTester.cs create mode 100644 Penumbra/Api/IpcTester/CollectionsIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/EditingIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/GameStateIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/IpcTester.cs create mode 100644 Penumbra/Api/IpcTester/MetaIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ModSettingsIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ModsIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/PluginStateIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/RedrawingIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ResolveIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/TemporaryIpcTester.cs create mode 100644 Penumbra/Api/IpcTester/UiIpcTester.cs delete mode 100644 Penumbra/Api/PenumbraApi.cs delete mode 100644 Penumbra/Api/PenumbraIpcProviders.cs create mode 100644 Penumbra/GuidExtensions.cs diff --git a/OtterGui b/OtterGui index 4673e93f..9599c806 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 4673e93f5165108a7f5b91236406d527f16384a5 +Subproject commit 9599c806877e2972f964dfa68e5207cf3a8f2b84 diff --git a/Penumbra.Api b/Penumbra.Api index 8787efc8..e5c8f544 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 8787efc8fc897dfbb4515ebbabbcd5e6f54d1b42 +Subproject commit e5c8f5446879e2e0e541eb4d8fee15e98b1885bc diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index c92a14fd..3446530a 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -24,7 +24,7 @@ public record struct VfxFuncInvokedEntry( string InvocationType, string CharacterName, string CharacterAddress, - string CollectionName) : ICrashDataEntry; + Guid CollectionId) : ICrashDataEntry; /// Only expose the write interface for the buffer. public interface IAnimationInvocationBufferWriter @@ -32,19 +32,19 @@ public interface IAnimationInvocationBufferWriter /// Write a line into the buffer with the given data. /// The address of the related character, if known. /// The name of the related character, anonymized or relying on index if unavailable, if known. - /// The name of the associated collection. Not anonymized. + /// The GUID of the associated collection. /// The type of VFX func called. - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type); + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, AnimationInvocationType type); } internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimationInvocationBufferWriter, IBufferReader { private const int _version = 1; private const int _lineCount = 64; - private const int _lineCapacity = 256; + private const int _lineCapacity = 128; private const string _name = "Penumbra.AnimationInvocation"; - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, AnimationInvocationType type) + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, AnimationInvocationType type) { var accessor = GetCurrentLineLocking(); lock (accessor) @@ -53,10 +53,10 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation accessor.Write(8, Environment.CurrentManagedThreadId); accessor.Write(12, (int)type); accessor.Write(16, characterAddress); - var span = GetSpan(accessor, 24, 104); + var span = GetSpan(accessor, 24, 16); + collectionId.TryWriteBytes(span); + span = GetSpan(accessor, 40); WriteSpan(characterName, span); - span = GetSpan(accessor, 128); - WriteString(collectionName, span); } } @@ -68,13 +68,13 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation var lineCount = (int)CurrentLineCount; for (var i = lineCount - 1; i >= 0; --i) { - var line = GetLine(i); - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); - var thread = BitConverter.ToInt32(line[8..]); - var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]); - var address = BitConverter.ToUInt64(line[16..]); - var characterName = ReadString(line[24..]); - var collectionName = ReadString(line[128..]); + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var type = (AnimationInvocationType)BitConverter.ToInt32(line[12..]); + var address = BitConverter.ToUInt64(line[16..]); + var collectionId = new Guid(line[24..40]); + var characterName = ReadString(line[40..]); yield return new JsonObject() { [nameof(VfxFuncInvokedEntry.Age)] = (crashTime - timestamp).TotalSeconds, @@ -83,7 +83,7 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation [nameof(VfxFuncInvokedEntry.InvocationType)] = ToName(type), [nameof(VfxFuncInvokedEntry.CharacterName)] = characterName, [nameof(VfxFuncInvokedEntry.CharacterAddress)] = address.ToString("X"), - [nameof(VfxFuncInvokedEntry.CollectionName)] = collectionName, + [nameof(VfxFuncInvokedEntry.CollectionId)] = collectionId, }; } } diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index d83c6e6c..4036455d 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -8,8 +8,8 @@ public interface ICharacterBaseBufferWriter /// Write a line into the buffer with the given data. /// The address of the related character, if known. /// The name of the related character, anonymized or relying on index if unavailable, if known. - /// The name of the associated collection. Not anonymized. - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName); + /// The GUID of the associated collection. + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId); } /// The full crash entry for a loaded character base. @@ -19,27 +19,27 @@ public record struct CharacterLoadedEntry( int ThreadId, string CharacterName, string CharacterAddress, - string CollectionName) : ICrashDataEntry; + Guid CollectionId) : ICrashDataEntry; internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBufferWriter, IBufferReader { - private const int _version = 1; - private const int _lineCount = 10; - private const int _lineCapacity = 256; - private const string _name = "Penumbra.CharacterBase"; + private const int _version = 1; + private const int _lineCount = 10; + private const int _lineCapacity = 128; + private const string _name = "Penumbra.CharacterBase"; - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName) + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId) { var accessor = GetCurrentLineLocking(); lock (accessor) { - accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); accessor.Write(12, characterAddress); - var span = GetSpan(accessor, 20, 108); + var span = GetSpan(accessor, 20, 16); + collectionId.TryWriteBytes(span); + span = GetSpan(accessor, 36); WriteSpan(characterName, span); - span = GetSpan(accessor, 128); - WriteString(collectionName, span); } } @@ -48,20 +48,20 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu var lineCount = (int)CurrentLineCount; for (var i = lineCount - 1; i >= 0; --i) { - var line = GetLine(i); - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); - var thread = BitConverter.ToInt32(line[8..]); - var address = BitConverter.ToUInt64(line[12..]); - var characterName = ReadString(line[20..]); - var collectionName = ReadString(line[128..]); + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var address = BitConverter.ToUInt64(line[12..]); + var collectionId = new Guid(line[20..36]); + var characterName = ReadString(line[36..]); yield return new JsonObject { - [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, - [nameof(CharacterLoadedEntry.Timestamp)] = timestamp, - [nameof(CharacterLoadedEntry.ThreadId)] = thread, - [nameof(CharacterLoadedEntry.CharacterName)] = characterName, + [nameof(CharacterLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(CharacterLoadedEntry.Timestamp)] = timestamp, + [nameof(CharacterLoadedEntry.ThreadId)] = thread, + [nameof(CharacterLoadedEntry.CharacterName)] = characterName, [nameof(CharacterLoadedEntry.CharacterAddress)] = address.ToString("X"), - [nameof(CharacterLoadedEntry.CollectionName)] = collectionName, + [nameof(CharacterLoadedEntry.CollectionId)] = collectionId, }; } } diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index 6c774e4b..03f63ba4 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -8,10 +8,10 @@ public interface IModdedFileBufferWriter /// Write a line into the buffer with the given data. /// The address of the related character, if known. /// The name of the related character, anonymized or relying on index if unavailable, if known. - /// The name of the associated collection. Not anonymized. + /// The GUID of the associated collection. /// The file name as requested by the game. /// The actual modded file name loaded. - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, ReadOnlySpan requestedFileName, ReadOnlySpan actualFileName); } @@ -22,33 +22,33 @@ public record struct ModdedFileLoadedEntry( int ThreadId, string CharacterName, string CharacterAddress, - string CollectionName, + Guid CollectionId, string RequestedFileName, string ActualFileName) : ICrashDataEntry; internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWriter, IBufferReader { - private const int _version = 1; - private const int _lineCount = 128; - private const int _lineCapacity = 1024; - private const string _name = "Penumbra.ModdedFile"; + private const int _version = 1; + private const int _lineCount = 128; + private const int _lineCapacity = 1024; + private const string _name = "Penumbra.ModdedFile"; - public void WriteLine(nint characterAddress, ReadOnlySpan characterName, string collectionName, ReadOnlySpan requestedFileName, + public void WriteLine(nint characterAddress, ReadOnlySpan characterName, Guid collectionId, ReadOnlySpan requestedFileName, ReadOnlySpan actualFileName) { var accessor = GetCurrentLineLocking(); lock (accessor) { - accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - accessor.Write(8, Environment.CurrentManagedThreadId); + accessor.Write(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + accessor.Write(8, Environment.CurrentManagedThreadId); accessor.Write(12, characterAddress); - var span = GetSpan(accessor, 20, 80); + var span = GetSpan(accessor, 20, 16); + collectionId.TryWriteBytes(span); + span = GetSpan(accessor, 36, 80); WriteSpan(characterName, span); - span = GetSpan(accessor, 92, 80); - WriteString(collectionName, span); - span = GetSpan(accessor, 172, 260); + span = GetSpan(accessor, 116, 260); WriteSpan(requestedFileName, span); - span = GetSpan(accessor, 432); + span = GetSpan(accessor, 376); WriteSpan(actualFileName, span); } } @@ -61,24 +61,24 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr var lineCount = (int)CurrentLineCount; for (var i = lineCount - 1; i >= 0; --i) { - var line = GetLine(i); - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); - var thread = BitConverter.ToInt32(line[8..]); - var address = BitConverter.ToUInt64(line[12..]); - var characterName = ReadString(line[20..]); - var collectionName = ReadString(line[92..]); - var requestedFileName = ReadString(line[172..]); - var actualFileName = ReadString(line[432..]); + var line = GetLine(i); + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(line)); + var thread = BitConverter.ToInt32(line[8..]); + var address = BitConverter.ToUInt64(line[12..]); + var collectionId = new Guid(line[20..36]); + var characterName = ReadString(line[36..]); + var requestedFileName = ReadString(line[116..]); + var actualFileName = ReadString(line[376..]); yield return new JsonObject() { - [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, - [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp, - [nameof(ModdedFileLoadedEntry.ThreadId)] = thread, - [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName, - [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"), - [nameof(ModdedFileLoadedEntry.CollectionName)] = collectionName, + [nameof(ModdedFileLoadedEntry.Age)] = (crashTime - timestamp).TotalSeconds, + [nameof(ModdedFileLoadedEntry.Timestamp)] = timestamp, + [nameof(ModdedFileLoadedEntry.ThreadId)] = thread, + [nameof(ModdedFileLoadedEntry.CharacterName)] = characterName, + [nameof(ModdedFileLoadedEntry.CharacterAddress)] = address.ToString("X"), + [nameof(ModdedFileLoadedEntry.CollectionId)] = collectionId, [nameof(ModdedFileLoadedEntry.RequestedFileName)] = requestedFileName, - [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName, + [nameof(ModdedFileLoadedEntry.ActualFileName)] = actualFileName, }; } } diff --git a/Penumbra.GameData b/Penumbra.GameData index 45679aa3..60222d79 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 45679aa32cc37b59f5eeb7cf6bf5a3ea36c626e0 +Subproject commit 60222d79420662fb8e9960a66e262a380fcaf186 diff --git a/Penumbra/Api/Api/ApiHelpers.cs b/Penumbra/Api/Api/ApiHelpers.cs new file mode 100644 index 00000000..32a3956f --- /dev/null +++ b/Penumbra/Api/Api/ApiHelpers.cs @@ -0,0 +1,76 @@ +using OtterGui.Log; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Api.Api; + +public class ApiHelpers( + CollectionManager collectionManager, + ObjectManager objects, + CollectionResolver collectionResolver, + ActorManager actors) : IApiService +{ + /// Return the associated identifier for an object given by its index. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal ActorIdentifier AssociatedIdentifier(int gameObjectIdx) + { + if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount) + return ActorIdentifier.Invalid; + + var ptr = objects[gameObjectIdx]; + return actors.FromObject(ptr, out _, false, true, true); + } + + /// + /// Return the collection associated to a current game object. If it does not exist, return the default collection. + /// If the index is invalid, returns false and the default collection. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) + { + collection = collectionManager.Active.Default; + if (gameObjectIdx < 0 || gameObjectIdx >= objects.TotalCount) + return false; + + var ptr = objects[gameObjectIdx]; + var data = collectionResolver.IdentifyCollection(ptr.AsObject, false); + if (data.Valid) + collection = data.ModCollection; + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") + { + Penumbra.Log.Debug( + $"[{name}] Called with {args}, returned {ec}."); + return ec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + internal static LazyString Args(params object[] arguments) + { + if (arguments.Length == 0) + return new LazyString(() => "no arguments"); + + return new LazyString(() => + { + var sb = new StringBuilder(); + for (var i = 0; i < arguments.Length / 2; ++i) + { + sb.Append(arguments[2 * i]); + sb.Append(" = "); + sb.Append(arguments[2 * i + 1]); + sb.Append(", "); + } + + return sb.ToString(0, sb.Length - 2); + }); + } +} diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs new file mode 100644 index 00000000..de704460 --- /dev/null +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -0,0 +1,141 @@ +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; + +namespace Penumbra.Api.Api; + +public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService +{ + public Dictionary GetCollections() + => collections.Storage.ToDictionary(c => c.Id, c => c.Name); + + public Dictionary GetChangedItemsForCollection(Guid collectionId) + { + try + { + if (!collections.Storage.ById(collectionId, out var collection)) + collection = ModCollection.Empty; + + if (collection.HasCache) + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); + + Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded."); + return []; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not obtain Changed Items for {collectionId}:\n{e}"); + throw; + } + } + + public (Guid Id, string Name)? GetCollection(ApiCollectionType type) + { + if (!Enum.IsDefined(type)) + return null; + + var collection = collections.Active.ByType((CollectionType)type); + return collection == null ? null : (collection.Id, collection.Name); + } + + internal (Guid Id, string Name)? GetCollection(byte type) + => GetCollection((ApiCollectionType)type); + + public (bool ObjectValid, bool IndividualSet, (Guid Id, string Name) EffectiveCollection) GetCollectionForObject(int gameObjectIdx) + { + var id = helpers.AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name)); + + if (collections.Active.Individuals.TryGetValue(id, out var collection)) + return (true, true, (collection.Id, collection.Name)); + + helpers.AssociatedCollection(gameObjectIdx, out collection); + return (true, false, (collection.Id, collection.Name)); + } + + public Guid[] GetCollectionByName(string name) + => collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray(); + + public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, + bool allowCreateNew, bool allowDelete) + { + if (!Enum.IsDefined(type)) + return (PenumbraApiEc.InvalidArgument, null); + + var oldCollection = collections.Active.ByType((CollectionType)type); + var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + if (collectionId == null) + { + if (old == null) + return (PenumbraApiEc.NothingChanged, old); + + if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) + return (PenumbraApiEc.AssignmentDeletionDisallowed, old); + + collections.Active.RemoveSpecialCollection((CollectionType)type); + return (PenumbraApiEc.Success, old); + } + + if (!collections.Storage.ById(collectionId.Value, out var collection)) + return (PenumbraApiEc.CollectionMissing, old); + + if (old == null) + { + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, old); + + collections.Active.CreateSpecialCollection((CollectionType)type); + } + else if (old.Value.Item1 == collection.Id) + { + return (PenumbraApiEc.NothingChanged, old); + } + + collections.Active.SetCollection(collection, (CollectionType)type); + return (PenumbraApiEc.Success, old); + } + + public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollectionForObject(int gameObjectIdx, Guid? collectionId, + bool allowCreateNew, bool allowDelete) + { + var id = helpers.AssociatedIdentifier(gameObjectIdx); + if (!id.IsValid) + return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name)); + + var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null; + var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + if (collectionId == null) + { + if (old == null) + return (PenumbraApiEc.NothingChanged, old); + + if (!allowDelete) + return (PenumbraApiEc.AssignmentDeletionDisallowed, old); + + var idx = collections.Active.Individuals.Index(id); + collections.Active.RemoveIndividualCollection(idx); + return (PenumbraApiEc.Success, old); + } + + if (!collections.Storage.ById(collectionId.Value, out var collection)) + return (PenumbraApiEc.CollectionMissing, old); + + if (old == null) + { + if (!allowCreateNew) + return (PenumbraApiEc.AssignmentCreationDisallowed, old); + + var ids = collections.Active.Individuals.GetGroup(id); + collections.Active.CreateIndividualCollection(ids); + } + else if (old.Value.Item1 == collection.Id) + { + return (PenumbraApiEc.NothingChanged, old); + } + + collections.Active.SetCollection(collection, CollectionType.Individual, collections.Active.Individuals.Index(id)); + return (PenumbraApiEc.Success, old); + } +} diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs new file mode 100644 index 00000000..93345053 --- /dev/null +++ b/Penumbra/Api/Api/EditingApi.cs @@ -0,0 +1,40 @@ +using OtterGui.Services; +using Penumbra.Import.Textures; +using TextureType = Penumbra.Api.Enums.TextureType; + +namespace Penumbra.Api.Api; + +public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IApiService +{ + public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => textureManager.SavePng(inputFile, outputFile), + TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), + TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), + TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), + TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile), + TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile), + TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), + TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), + TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + + // @formatter:off + public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps) + => textureType switch + { + TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.RgbaDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), + }; + // @formatter:on +} diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs new file mode 100644 index 00000000..becb55ee --- /dev/null +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -0,0 +1,82 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Structs; +using Penumbra.Services; +using Penumbra.String.Classes; + +namespace Penumbra.Api.Api; + +public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly CollectionResolver _collectionResolver; + private readonly CutsceneService _cutsceneService; + private readonly ResourceLoader _resourceLoader; + + public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService, + ResourceLoader resourceLoader) + { + _communicator = communicator; + _collectionResolver = collectionResolver; + _cutsceneService = cutsceneService; + _resourceLoader = resourceLoader; + _resourceLoader.ResourceLoaded += OnResourceLoaded; + _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); + } + + public unsafe void Dispose() + { + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); + } + + public event CreatedCharacterBaseDelegate? CreatedCharacterBase; + public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; + + public event CreatingCharacterBaseDelegate? CreatingCharacterBase + { + add + { + if (value == null) + return; + + _communicator.CreatingCharacterBase.Subscribe(new Action(value), + Communication.CreatingCharacterBase.Priority.Api); + } + remove + { + if (value == null) + return; + + _communicator.CreatingCharacterBase.Unsubscribe(new Action(value)); + } + } + + public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject) + { + var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name)); + } + + public int GetCutsceneParentIndex(int actorIdx) + => _cutsceneService.GetParentIndex(actorIdx); + + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) + => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) + ? PenumbraApiEc.Success + : PenumbraApiEc.InvalidArgument; + + private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) + { + if (resolveData.AssociatedGameObject != nint.Zero) + GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), + manipulatedPath?.ToString() ?? originalPath.ToString()); + } + + private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) + => CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject); +} diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs new file mode 100644 index 00000000..c467df58 --- /dev/null +++ b/Penumbra/Api/Api/MetaApi.cs @@ -0,0 +1,23 @@ +using OtterGui; +using OtterGui.Services; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Api.Api; + +public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService +{ + public string GetPlayerMetaManipulations() + { + var collection = collectionResolver.PlayerCollection(); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + } + + public string GetMetaManipulations(int gameObjectIdx) + { + helpers.AssociatedCollection(gameObjectIdx, out var collection); + var set = collection.MetaCache?.Manipulations.ToArray() ?? []; + return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + } +} diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs new file mode 100644 index 00000000..2604a49d --- /dev/null +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -0,0 +1,282 @@ +using OtterGui; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Interop.PathResolving; +using Penumbra.Mods; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Subclasses; +using Penumbra.Services; + +namespace Penumbra.Api.Api; + +public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable +{ + private readonly CollectionResolver _collectionResolver; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly CollectionEditor _collectionEditor; + private readonly CommunicatorService _communicator; + + public ModSettingsApi(CollectionResolver collectionResolver, + ModManager modManager, + CollectionManager collectionManager, + CollectionEditor collectionEditor, + CommunicatorService communicator) + { + _collectionResolver = collectionResolver; + _modManager = modManager; + _collectionManager = collectionManager; + _collectionEditor = collectionEditor; + _communicator = communicator; + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); + _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); + _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api); + } + + public void Dispose() + { + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); + } + + public event ModSettingChangedDelegate? ModSettingChanged; + + public AvailableModSettings? GetAvailableModSettings(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return null; + + var dict = new Dictionary(mod.Groups.Count); + foreach (var g in mod.Groups) + dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type)); + return new AvailableModSettings(dict); + } + + public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) + => _modManager.TryGetMod(modDirectory, modName, out var mod) + ? mod.Groups.ToDictionary(g => g.Name, g => (g.Select(o => o.Name).ToArray(), (int)g.Type)) + : null; + + public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, + string modName, bool ignoreInheritance) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return (PenumbraApiEc.ModMissing, null); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return (PenumbraApiEc.CollectionMissing, null); + + var settings = collection.Id == Guid.Empty + ? null + : ignoreInheritance + ? collection.Settings[mod.Index] + : collection[mod.Index].Settings; + if (settings == null) + return (PenumbraApiEc.Success, null); + + var (enabled, priority, dict) = settings.ConvertToShareable(mod); + return (PenumbraApiEc.Success, + (enabled, priority.Value, dict, collection.Settings[mod.Index] == null)); + } + + public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", + inherit.ToString()); + + if (collectionId == Guid.Empty) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var ret = _collectionEditor.SetModInheritance(collection, mod, inherit) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetMod(Guid collectionId, string modDirectory, string modName, bool enabled) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var ret = _collectionEditor.SetModState(collection, mod, enabled) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetModPriority(Guid collectionId, string modDirectory, string modName, int priority) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Priority", priority); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var ret = _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority)) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetModSetting(Guid collectionId, string modDirectory, string modName, string optionGroupName, string optionName) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", + optionGroupName, "OptionName", optionName); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + var setting = mod.Groups[groupIdx] switch + { + MultiModGroup => Setting.Multi(optionIdx), + SingleModGroup => Setting.Single(optionIdx), + _ => Setting.Zero, + }; + var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc TrySetModSettings(Guid collectionId, string modDirectory, string modName, string optionGroupName, + IReadOnlyList optionNames) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", + optionGroupName, "#optionNames", optionNames.Count); + + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var setting = Setting.Zero; + switch (mod.Groups[groupIdx]) + { + case SingleModGroup single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting = Setting.Single(optionIdx); + break; + } + case MultiModGroup multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc CopyModSettings(Guid? collectionId, string modDirectoryFrom, string modDirectoryTo) + { + var args = ApiHelpers.Args("CollectionId", collectionId.HasValue ? collectionId.Value.ToString() : "NULL", + "From", modDirectoryFrom, "To", modDirectoryTo); + var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase)); + var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase)); + if (collectionId == null) + foreach (var collection in _collectionManager.Storage) + _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); + else if (_collectionManager.Storage.ById(collectionId.Value, out var collection)) + _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); + else + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void TriggerSettingEdited(Mod mod) + { + var collection = _collectionResolver.PlayerCollection(); + var (settings, parent) = collection[mod.Index]; + if (settings is { Enabled: true }) + ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection); + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + { + if (type == ModPathChangeType.Reloaded) + TriggerSettingEdited(mod); + } + + private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) + => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); + + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) + { + switch (type) + { + case ModOptionChangeType.GroupDeleted: + case ModOptionChangeType.GroupMoved: + case ModOptionChangeType.GroupTypeChanged: + case ModOptionChangeType.PriorityChanged: + case ModOptionChangeType.OptionDeleted: + case ModOptionChangeType.OptionMoved: + case ModOptionChangeType.OptionFilesChanged: + case ModOptionChangeType.OptionFilesAdded: + case ModOptionChangeType.OptionSwapsChanged: + case ModOptionChangeType.OptionMetaChanged: + TriggerSettingEdited(mod); + break; + } + } + + private void OnModFileChanged(Mod mod, FileRegistry file) + { + if (file.CurrentUsage == 0) + return; + + TriggerSettingEdited(mod); + } +} diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs new file mode 100644 index 00000000..c1e0c684 --- /dev/null +++ b/Penumbra/Api/Api/ModsApi.cs @@ -0,0 +1,132 @@ +using OtterGui.Compression; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; + +namespace Penumbra.Api.Api; + +public class ModsApi : IPenumbraApiMods, IApiService, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ModManager _modManager; + private readonly ModImportManager _modImportManager; + private readonly Configuration _config; + private readonly ModFileSystem _modFileSystem; + + public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem, + CommunicatorService communicator) + { + _modManager = modManager; + _modImportManager = modImportManager; + _config = config; + _modFileSystem = modFileSystem; + _communicator = communicator; + _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods); + } + + private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) + { + 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.Moved when newDirectory != null && oldDirectory != null: + ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name); + break; + } + } + + public void Dispose() + => _communicator.ModPathChanged.Unsubscribe(OnModPathChanged); + + public Dictionary GetModList() + => _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text); + + public PenumbraApiEc InstallMod(string modFilePackagePath) + { + if (!File.Exists(modFilePackagePath)) + return ApiHelpers.Return(PenumbraApiEc.FileMissing, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath)); + + _modImportManager.AddUnpack(modFilePackagePath); + return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModFilePackagePath", modFilePackagePath)); + } + + public PenumbraApiEc ReloadMod(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + + _modManager.ReloadMod(mod); + return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + } + + public PenumbraApiEc AddMod(string modDirectory) + { + var args = ApiHelpers.Args("ModDirectory", modDirectory); + + var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); + if (!dir.Exists) + return ApiHelpers.Return(PenumbraApiEc.FileMissing, args); + + if (_modManager.BasePath.FullName != dir.Parent?.FullName) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + _modManager.AddMod(dir); + if (_config.UseFileSystemCompression) + new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), + CompressionAlgorithm.Xpress8K); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc DeleteMod(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + + _modManager.DeleteMod(mod); + return ApiHelpers.Return(PenumbraApiEc.Success, ApiHelpers.Args("ModDirectory", modDirectory, "ModName", modName)); + } + + public event Action? ModDeleted; + public event Action? ModAdded; + public event Action? ModMoved; + + public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) + || !_modFileSystem.FindLeaf(mod, out var leaf)) + return (PenumbraApiEc.ModMissing, string.Empty, false, false); + + var fullPath = leaf.FullName(); + var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath); + var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name); + return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault ); + } + + public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) + { + if (newPath.Length == 0) + return PenumbraApiEc.InvalidArgument; + + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) + || !_modFileSystem.FindLeaf(mod, out var leaf)) + return PenumbraApiEc.ModMissing; + + try + { + _modFileSystem.RenameAndMove(leaf, newPath); + return PenumbraApiEc.Success; + } + catch + { + return PenumbraApiEc.PathRenameFailed; + } + } +} diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs new file mode 100644 index 00000000..1d5b1537 --- /dev/null +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -0,0 +1,40 @@ +using OtterGui.Services; + +namespace Penumbra.Api.Api; + +public class PenumbraApi( + CollectionApi collection, + EditingApi editing, + GameStateApi gameState, + MetaApi meta, + ModsApi mods, + ModSettingsApi modSettings, + PluginStateApi pluginState, + RedrawApi redraw, + ResolveApi resolve, + ResourceTreeApi resourceTree, + TemporaryApi temporary, + UiApi ui) : IDisposable, IApiService, IPenumbraApi +{ + public void Dispose() + { + Valid = false; + } + + public (int Breaking, int Feature) ApiVersion + => (5, 0); + + public bool Valid { get; private set; } = true; + public IPenumbraApiCollection Collection { get; } = collection; + public IPenumbraApiEditing Editing { get; } = editing; + public IPenumbraApiGameState GameState { get; } = gameState; + public IPenumbraApiMeta Meta { get; } = meta; + public IPenumbraApiMods Mods { get; } = mods; + public IPenumbraApiModSettings ModSettings { get; } = modSettings; + public IPenumbraApiPluginState PluginState { get; } = pluginState; + public IPenumbraApiRedraw Redraw { get; } = redraw; + public IPenumbraApiResolve Resolve { get; } = resolve; + public IPenumbraApiResourceTree ResourceTree { get; } = resourceTree; + public IPenumbraApiTemporary Temporary { get; } = temporary; + public IPenumbraApiUi Ui { get; } = ui; +} diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs new file mode 100644 index 00000000..2e87486f --- /dev/null +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using OtterGui.Services; +using Penumbra.Communication; +using Penumbra.Services; + +namespace Penumbra.Api.Api; + +public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService +{ + public string GetModDirectory() + => config.ModDirectory; + + public string GetConfiguration() + => JsonConvert.SerializeObject(config, Formatting.Indented); + + public event Action? ModDirectoryChanged + { + add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); + remove => communicator.ModDirectoryChanged.Unsubscribe(value!); + } + + public bool GetEnabledState() + => config.EnableMods; + + public event Action? EnabledChange + { + add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); + remove => communicator.EnabledChanged.Unsubscribe(value!); + } +} diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs new file mode 100644 index 00000000..03b42493 --- /dev/null +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -0,0 +1,27 @@ +using Dalamud.Game.ClientState.Objects.Types; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Interop.Services; + +namespace Penumbra.Api.Api; + +public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService +{ + public void RedrawObject(int gameObjectIndex, RedrawType setting) + => redrawService.RedrawObject(gameObjectIndex, setting); + + public void RedrawObject(string name, RedrawType setting) + => redrawService.RedrawObject(name, setting); + + public void RedrawObject(GameObject? gameObject, RedrawType setting) + => redrawService.RedrawObject(gameObject, setting); + + public void RedrawAll(RedrawType setting) + => redrawService.RedrawAll(setting); + + public event GameObjectRedrawnDelegate? GameObjectRedrawn + { + add => redrawService.GameObjectRedrawn += value; + remove => redrawService.GameObjectRedrawn -= value; + } +} diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs new file mode 100644 index 00000000..ec57eba7 --- /dev/null +++ b/Penumbra/Api/Api/ResolveApi.cs @@ -0,0 +1,101 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Interop.PathResolving; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; + +namespace Penumbra.Api.Api; + +public class ResolveApi( + ModManager modManager, + CollectionManager collectionManager, + Configuration config, + CollectionResolver collectionResolver, + ApiHelpers helpers, + IFramework framework) : IPenumbraApiResolve, IApiService +{ + public string ResolveDefaultPath(string gamePath) + => ResolvePath(gamePath, modManager, collectionManager.Active.Default); + + public string ResolveInterfacePath(string gamePath) + => ResolvePath(gamePath, modManager, collectionManager.Active.Interface); + + public string ResolveGameObjectPath(string gamePath, int gameObjectIdx) + { + helpers.AssociatedCollection(gameObjectIdx, out var collection); + return ResolvePath(gamePath, modManager, collection); + } + + public string ResolvePlayerPath(string gamePath) + => ResolvePath(gamePath, modManager, collectionResolver.PlayerCollection()); + + public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIdx) + { + if (!config.EnableMods) + return [moddedPath]; + + helpers.AssociatedCollection(gameObjectIdx, out var collection); + var ret = collection.ReverseResolvePath(new FullPath(moddedPath)); + return ret.Select(r => r.ToString()).ToArray(); + } + + public string[] ReverseResolvePlayerPath(string moddedPath) + { + if (!config.EnableMods) + return [moddedPath]; + + var ret = collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(moddedPath)); + return ret.Select(r => r.ToString()).ToArray(); + } + + public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) + { + if (!config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); + + var playerCollection = collectionResolver.PlayerCollection(); + var resolved = forward.Select(p => ResolvePath(p, modManager, playerCollection)).ToArray(); + var reverseResolved = playerCollection.ReverseResolvePaths(reverse); + return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); + } + + public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) + { + if (!config.EnableMods) + return (forward, reverse.Select(p => new[] + { + p, + }).ToArray()); + + return await Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false); + var forwardTask = Task.Run(() => + { + var forwardRet = new string[forward.Length]; + Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], modManager, playerCollection)); + return forwardRet; + }).ConfigureAwait(false); + var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false); + var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); + return (await forwardTask, reverseResolved); + }).ConfigureAwait(false); + } + + /// Resolve a path given by string for a specific collection. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private string ResolvePath(string path, ModManager _, ModCollection collection) + { + if (!config.EnableMods) + return path; + + var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; + var ret = collection.ResolvePath(gamePath); + return ret?.ToString() ?? path; + } +} diff --git a/Penumbra/Api/Api/ResourceTreeApi.cs b/Penumbra/Api/Api/ResourceTreeApi.cs new file mode 100644 index 00000000..6e9aaa48 --- /dev/null +++ b/Penumbra/Api/Api/ResourceTreeApi.cs @@ -0,0 +1,63 @@ +using Dalamud.Game.ClientState.Objects.Types; +using Newtonsoft.Json.Linq; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.GameData.Interop; +using Penumbra.Interop.ResourceTree; + +namespace Penumbra.Api.Api; + +public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectManager objects) : IPenumbraApiResourceTree, IApiService +{ + public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0); + var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); + + return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj)); + } + + public Dictionary>> GetPlayerResourcePaths() + { + var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly); + return ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); + } + + public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, + params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); + var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + + return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj)); + } + + public Dictionary GetPlayerResourcesOfType(ResourceType type, + bool withUiData) + { + var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); + return ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); + } + + public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) + { + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj)); + } + + public Dictionary GetPlayerResourceTrees(bool withUiData) + { + var resourceTrees = resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly + | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); + var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); + + return resDictionary; + } +} diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs new file mode 100644 index 00000000..b4ffa8f4 --- /dev/null +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -0,0 +1,190 @@ +using OtterGui; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Interop; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; +using Penumbra.String.Classes; + +namespace Penumbra.Api.Api; + +public class TemporaryApi( + TempCollectionManager tempCollections, + ObjectManager objects, + ActorManager actors, + CollectionManager collectionManager, + TempModManager tempMods) : IPenumbraApiTemporary, IApiService +{ + public Guid CreateTemporaryCollection(string name) + => tempCollections.CreateTemporaryCollection(name); + + public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId) + => tempCollections.RemoveTemporaryCollection(collectionId) + ? PenumbraApiEc.Success + : PenumbraApiEc.CollectionMissing; + + public PenumbraApiEc AssignTemporaryCollection(Guid collectionId, int actorIndex, bool forceAssignment) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ActorIndex", actorIndex, "Forced", forceAssignment); + if (actorIndex < 0 || actorIndex >= objects.TotalCount) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + var identifier = actors.FromObject(objects[actorIndex], out _, false, false, true); + if (!identifier.IsValid) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + if (!tempCollections.CollectionById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (forceAssignment) + { + if (tempCollections.Collections.ContainsKey(identifier) && !tempCollections.Collections.Delete(identifier)) + return ApiHelpers.Return(PenumbraApiEc.AssignmentDeletionFailed, args); + } + else if (tempCollections.Collections.ContainsKey(identifier) + || collectionManager.Active.Individuals.ContainsKey(identifier)) + { + return ApiHelpers.Return(PenumbraApiEc.CharacterCollectionExists, args); + } + + var group = tempCollections.Collections.GetGroup(identifier); + var ret = tempCollections.AddIdentifier(collection, group) + ? PenumbraApiEc.Success + : PenumbraApiEc.UnknownError; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority) + { + var args = ApiHelpers.Args("Tag", tag, "#Paths", paths.Count, "ManipString", manipString, "Priority", priority); + if (!ConvertPaths(paths, out var p)) + return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); + + if (!ConvertManips(manipString, out var m)) + return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); + + var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc AddTemporaryMod(string tag, Guid collectionId, Dictionary paths, string manipString, int priority) + { + var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "#Paths", paths.Count, "ManipString", + manipString, "Priority", priority); + + if (collectionId == Guid.Empty) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + if (!tempCollections.CollectionById(collectionId, out var collection) + && !collectionManager.Storage.ById(collectionId, out collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + if (!ConvertPaths(paths, out var p)) + return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); + + if (!ConvertManips(manipString, out var m)) + return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); + + var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, args); + } + + public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) + { + var ret = tempMods.Unregister(tag, null, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, ApiHelpers.Args("Tag", tag, "Priority", priority)); + } + + public PenumbraApiEc RemoveTemporaryMod(string tag, Guid collectionId, int priority) + { + var args = ApiHelpers.Args("Tag", tag, "CollectionId", collectionId, "Priority", priority); + + if (!tempCollections.CollectionById(collectionId, out var collection) + && !collectionManager.Storage.ById(collectionId, out collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + var ret = tempMods.Unregister(tag, collection, new ModPriority(priority)) switch + { + RedirectResult.Success => PenumbraApiEc.Success, + RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, + _ => PenumbraApiEc.UnknownError, + }; + return ApiHelpers.Return(ret, args); + } + + /// + /// Convert a dictionary of strings to a dictionary of game paths to full paths. + /// Only returns true if all paths can successfully be converted and added. + /// + private static bool ConvertPaths(IReadOnlyDictionary redirections, + [NotNullWhen(true)] out Dictionary? paths) + { + paths = new Dictionary(redirections.Count); + foreach (var (gString, fString) in redirections) + { + if (!Utf8GamePath.FromString(gString, out var path, false)) + { + paths = null; + return false; + } + + var fullPath = new FullPath(fString); + if (!paths.TryAdd(path, fullPath)) + { + paths = null; + return false; + } + } + + return true; + } + + /// + /// Convert manipulations from a transmitted base64 string to actual manipulations. + /// The empty string is treated as an empty set. + /// Only returns true if all conversions are successful and distinct. + /// + private static bool ConvertManips(string manipString, + [NotNullWhen(true)] out HashSet? manips) + { + if (manipString.Length == 0) + { + manips = []; + return true; + } + + if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) + { + manips = null; + return false; + } + + manips = new HashSet(manipArray!.Length); + foreach (var manip in manipArray.Where(m => m.Validate())) + { + if (manips.Add(manip)) + continue; + + Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); + manips = null; + return false; + } + + return true; + } +} diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs new file mode 100644 index 00000000..cf3cd8f2 --- /dev/null +++ b/Penumbra/Api/Api/UiApi.cs @@ -0,0 +1,101 @@ +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.GameData.Enums; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI; + +namespace Penumbra.Api.Api; + +public class UiApi : IPenumbraApiUi, IApiService, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ConfigWindow _configWindow; + private readonly ModManager _modManager; + + public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager) + { + _communicator = communicator; + _configWindow = configWindow; + _modManager = modManager; + _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); + _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); + } + + public void Dispose() + { + _communicator.ChangedItemHover.Unsubscribe(OnChangedItemHover); + _communicator.ChangedItemClick.Unsubscribe(OnChangedItemClick); + } + + public event Action? ChangedItemTooltip; + + public event Action? ChangedItemClicked; + + public event Action? PreSettingsTabBarDraw + { + add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); + remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); + } + + public event Action? PreSettingsPanelDraw + { + add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); + remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); + } + + public event Action? PostEnabledDraw + { + add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default); + remove => _communicator.PostEnabledDraw.Unsubscribe(value!); + } + + public event Action? PostSettingsPanelDraw + { + add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default); + remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!); + } + + public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) + { + _configWindow.IsOpen = true; + if (!Enum.IsDefined(tab)) + return PenumbraApiEc.InvalidArgument; + + if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) + { + if (_modManager.TryGetMod(modDirectory, modName, out var mod)) + _communicator.SelectTab.Invoke(tab, mod); + else + return PenumbraApiEc.ModMissing; + } + else if (tab != TabType.None) + { + _communicator.SelectTab.Invoke(tab, null); + } + + return PenumbraApiEc.Success; + } + + public void CloseMainWindow() + => _configWindow.IsOpen = false; + + private void OnChangedItemClick(MouseButton button, object? data) + { + if (ChangedItemClicked == null) + return; + + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + ChangedItemClicked.Invoke(button, type, id); + } + + private void OnChangedItemHover(object? data) + { + if (ChangedItemTooltip == null) + return; + + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + ChangedItemTooltip.Invoke(type, id); + } +} diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 0374e31a..1c2cebcc 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -9,7 +10,7 @@ using Penumbra.String.Classes; namespace Penumbra.Api; -public class DalamudSubstitutionProvider : IDisposable +public class DalamudSubstitutionProvider : IDisposable, IApiService { private readonly ITextureSubstitutionProvider _substitution; private readonly ActiveCollectionData _activeCollectionData; diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index e23f8b4f..859c46b4 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -1,11 +1,13 @@ using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; +using OtterGui.Services; +using Penumbra.Api.Api; using Penumbra.Api.Enums; namespace Penumbra.Api; -public class HttpApi : IDisposable +public class HttpApi : IDisposable, IApiService { private partial class Controller : WebApiController { @@ -67,7 +69,7 @@ public class HttpApi : IDisposable public partial object? GetMods() { Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); - return _api.GetModList(); + return _api.Mods.GetModList(); } public async partial Task Redraw() @@ -75,17 +77,15 @@ public class HttpApi : IDisposable var data = await HttpContext.GetRequestDataAsync(); Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}."); if (data.ObjectTableIndex >= 0) - _api.RedrawObject(data.ObjectTableIndex, data.Type); - else if (data.Name.Length > 0) - _api.RedrawObject(data.Name, data.Type); + _api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); else - _api.RedrawAll(data.Type); + _api.Redraw.RedrawAll(data.Type); } public partial void RedrawAll() { Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered."); - _api.RedrawAll(RedrawType.Redraw); + _api.Redraw.RedrawAll(RedrawType.Redraw); } public async partial Task ReloadMod() @@ -95,10 +95,10 @@ public class HttpApi : IDisposable // Add the mod if it is not already loaded and if the directory name is given. // AddMod returns Success if the mod is already loaded. if (data.Path.Length != 0) - _api.AddMod(data.Path); + _api.Mods.AddMod(data.Path); // Reload the mod by path or name, which will also remove no-longer existing mods. - _api.ReloadMod(data.Path, data.Name); + _api.Mods.ReloadMod(data.Path, data.Name); } public async partial Task InstallMod() @@ -106,13 +106,13 @@ public class HttpApi : IDisposable var data = await HttpContext.GetRequestDataAsync(); Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}."); if (data.Path.Length != 0) - _api.InstallMod(data.Path); + _api.Mods.InstallMod(data.Path); } public partial void OpenWindow() { Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); - _api.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); + _api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } private record ModReloadData(string Path, string Name) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs new file mode 100644 index 00000000..293af588 --- /dev/null +++ b/Penumbra/Api/IpcProviders.cs @@ -0,0 +1,118 @@ +using Dalamud.Plugin; +using OtterGui.Services; +using Penumbra.Api.Api; +using Penumbra.Api.Helpers; + +namespace Penumbra.Api; + +public sealed class IpcProviders : IDisposable, IApiService +{ + private readonly List _providers; + + private readonly EventProvider _disposedProvider; + private readonly EventProvider _initializedProvider; + + public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api) + { + _disposedProvider = IpcSubscribers.Disposed.Provider(pi); + _initializedProvider = IpcSubscribers.Initialized.Provider(pi); + _providers = + [ + IpcSubscribers.GetCollections.Provider(pi, api.Collection), + IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection), + IpcSubscribers.GetCollection.Provider(pi, api.Collection), + IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection), + IpcSubscribers.SetCollection.Provider(pi, api.Collection), + IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection), + + IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing), + IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing), + + IpcSubscribers.GetDrawObjectInfo.Provider(pi, api.GameState), + IpcSubscribers.GetCutsceneParentIndex.Provider(pi, api.GameState), + IpcSubscribers.SetCutsceneParentIndex.Provider(pi, api.GameState), + IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState), + IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState), + IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState), + + IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta), + IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta), + + IpcSubscribers.GetModList.Provider(pi, api.Mods), + IpcSubscribers.InstallMod.Provider(pi, api.Mods), + IpcSubscribers.ReloadMod.Provider(pi, api.Mods), + IpcSubscribers.AddMod.Provider(pi, api.Mods), + IpcSubscribers.DeleteMod.Provider(pi, api.Mods), + IpcSubscribers.ModDeleted.Provider(pi, api.Mods), + IpcSubscribers.ModAdded.Provider(pi, api.Mods), + IpcSubscribers.ModMoved.Provider(pi, api.Mods), + IpcSubscribers.GetModPath.Provider(pi, api.Mods), + IpcSubscribers.SetModPath.Provider(pi, api.Mods), + + IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetModSetting.Provider(pi, api.ModSettings), + IpcSubscribers.TrySetModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.ModSettingChanged.Provider(pi, api.ModSettings), + IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings), + + IpcSubscribers.ApiVersion.Provider(pi, api), + IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), + IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), + IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), + IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState), + IpcSubscribers.EnabledChange.Provider(pi, api.PluginState), + + IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), + IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), + IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw), + + IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve), + IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve), + IpcSubscribers.ResolveGameObjectPath.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePlayerPath.Provider(pi, api.Resolve), + IpcSubscribers.ReverseResolveGameObjectPath.Provider(pi, api.Resolve), + IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve), + + IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree), + IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree), + IpcSubscribers.GetGameObjectResourcesOfType.Provider(pi, api.ResourceTree), + IpcSubscribers.GetPlayerResourcesOfType.Provider(pi, api.ResourceTree), + IpcSubscribers.GetGameObjectResourceTrees.Provider(pi, api.ResourceTree), + IpcSubscribers.GetPlayerResourceTrees.Provider(pi, api.ResourceTree), + + IpcSubscribers.CreateTemporaryCollection.Provider(pi, api.Temporary), + IpcSubscribers.DeleteTemporaryCollection.Provider(pi, api.Temporary), + IpcSubscribers.AssignTemporaryCollection.Provider(pi, api.Temporary), + IpcSubscribers.AddTemporaryModAll.Provider(pi, api.Temporary), + IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary), + + IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), + IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui), + IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), + IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), + ]; + _initializedProvider.Invoke(); + } + + public void Dispose() + { + foreach (var provider in _providers) + provider.Dispose(); + _providers.Clear(); + _initializedProvider.Dispose(); + _disposedProvider.Invoke(); + _disposedProvider.Dispose(); + } +} diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs deleted file mode 100644 index 898c5de3..00000000 --- a/Penumbra/Api/IpcTester.cs +++ /dev/null @@ -1,1762 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using Dalamud.Plugin; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Mods; -using Dalamud.Utility; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.UI; -using Penumbra.Collections.Manager; -using Dalamud.Plugin.Services; -using Penumbra.GameData.Structs; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; - -namespace Penumbra.Api; - -public class IpcTester : IDisposable -{ - private readonly PenumbraIpcProviders _ipcProviders; - private bool _subscribed = true; - - private readonly PluginState _pluginState; - private readonly IpcConfiguration _ipcConfiguration; - private readonly Ui _ui; - private readonly Redrawing _redrawing; - private readonly GameState _gameState; - private readonly Resolve _resolve; - private readonly Collections _collections; - private readonly Meta _meta; - private readonly Mods _mods; - private readonly ModSettings _modSettings; - private readonly Editing _editing; - private readonly Temporary _temporary; - private readonly ResourceTree _resourceTree; - - public IpcTester(Configuration config, DalamudPluginInterface pi, ObjectManager objects, IClientState clientState, - PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections, TempModManager tempMods, - TempCollectionManager tempCollections, SaveService saveService) - { - _ipcProviders = ipcProviders; - _pluginState = new PluginState(pi); - _ipcConfiguration = new IpcConfiguration(pi); - _ui = new Ui(pi); - _redrawing = new Redrawing(pi, objects, clientState); - _gameState = new GameState(pi); - _resolve = new Resolve(pi); - _collections = new Collections(pi); - _meta = new Meta(pi); - _mods = new Mods(pi); - _modSettings = new ModSettings(pi); - _editing = new Editing(pi); - _temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService, config); - _resourceTree = new ResourceTree(pi, objects); - UnsubscribeEvents(); - } - - public void Draw() - { - try - { - SubscribeEvents(); - ImGui.TextUnformatted($"API Version: {_ipcProviders.Api.ApiVersion.Breaking}.{_ipcProviders.Api.ApiVersion.Feature:D4}"); - _pluginState.Draw(); - _ipcConfiguration.Draw(); - _ui.Draw(); - _redrawing.Draw(); - _gameState.Draw(); - _resolve.Draw(); - _collections.Draw(); - _meta.Draw(); - _mods.Draw(); - _modSettings.Draw(); - _editing.Draw(); - _temporary.Draw(); - _temporary.DrawCollections(); - _temporary.DrawMods(); - _resourceTree.Draw(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Error during IPC Tests:\n{e}"); - } - } - - private void SubscribeEvents() - { - if (!_subscribed) - { - _pluginState.Initialized.Enable(); - _pluginState.Disposed.Enable(); - _pluginState.EnabledChange.Enable(); - _redrawing.Redrawn.Enable(); - _ui.PreSettingsDraw.Enable(); - _ui.PostSettingsDraw.Enable(); - _modSettings.SettingChanged.Enable(); - _gameState.CharacterBaseCreating.Enable(); - _gameState.CharacterBaseCreated.Enable(); - _ipcConfiguration.ModDirectoryChanged.Enable(); - _gameState.GameObjectResourcePathResolved.Enable(); - _mods.DeleteSubscriber.Enable(); - _mods.AddSubscriber.Enable(); - _mods.MoveSubscriber.Enable(); - _subscribed = true; - } - } - - public void UnsubscribeEvents() - { - if (_subscribed) - { - _pluginState.Initialized.Disable(); - _pluginState.Disposed.Disable(); - _pluginState.EnabledChange.Disable(); - _redrawing.Redrawn.Disable(); - _ui.PreSettingsDraw.Disable(); - _ui.PostSettingsDraw.Disable(); - _ui.Tooltip.Disable(); - _ui.Click.Disable(); - _modSettings.SettingChanged.Disable(); - _gameState.CharacterBaseCreating.Disable(); - _gameState.CharacterBaseCreated.Disable(); - _ipcConfiguration.ModDirectoryChanged.Disable(); - _gameState.GameObjectResourcePathResolved.Disable(); - _mods.DeleteSubscriber.Disable(); - _mods.AddSubscriber.Disable(); - _mods.MoveSubscriber.Disable(); - _subscribed = false; - } - } - - public void Dispose() - { - _pluginState.Initialized.Dispose(); - _pluginState.Disposed.Dispose(); - _pluginState.EnabledChange.Dispose(); - _redrawing.Redrawn.Dispose(); - _ui.PreSettingsDraw.Dispose(); - _ui.PostSettingsDraw.Dispose(); - _ui.Tooltip.Dispose(); - _ui.Click.Dispose(); - _modSettings.SettingChanged.Dispose(); - _gameState.CharacterBaseCreating.Dispose(); - _gameState.CharacterBaseCreated.Dispose(); - _ipcConfiguration.ModDirectoryChanged.Dispose(); - _gameState.GameObjectResourcePathResolved.Dispose(); - _mods.DeleteSubscriber.Dispose(); - _mods.AddSubscriber.Dispose(); - _mods.MoveSubscriber.Dispose(); - _subscribed = false; - } - - private static void DrawIntro(string label, string info) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(label); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(info); - ImGui.TableNextColumn(); - } - - - private class PluginState - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber Initialized; - public readonly EventSubscriber Disposed; - public readonly EventSubscriber EnabledChange; - - private readonly List _initializedList = new(); - private readonly List _disposedList = new(); - - private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; - private bool? _lastEnabledValue; - - public PluginState(DalamudPluginInterface pi) - { - _pi = pi; - Initialized = Ipc.Initialized.Subscriber(pi, AddInitialized); - Disposed = Ipc.Disposed.Subscriber(pi, AddDisposed); - EnabledChange = Ipc.EnabledChange.Subscriber(pi, SetLastEnabled); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Plugin State"); - if (!_) - return; - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - void DrawList(string label, string text, List list) - { - DrawIntro(label, text); - if (list.Count == 0) - { - ImGui.TextUnformatted("Never"); - } - else - { - ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture)); - if (list.Count > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", - list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture)))); - } - } - - DrawList(Ipc.Initialized.Label, "Last Initialized", _initializedList); - DrawList(Ipc.Disposed.Label, "Last Disposed", _disposedList); - DrawIntro(Ipc.ApiVersions.Label, "Current Version"); - var (breaking, features) = Ipc.ApiVersions.Subscriber(_pi).Invoke(); - ImGui.TextUnformatted($"{breaking}.{features:D4}"); - DrawIntro(Ipc.GetEnabledState.Label, "Current State"); - ImGui.TextUnformatted($"{Ipc.GetEnabledState.Subscriber(_pi).Invoke()}"); - DrawIntro(Ipc.EnabledChange.Label, "Last Change"); - ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); - } - - private void AddInitialized() - => _initializedList.Add(DateTimeOffset.UtcNow); - - private void AddDisposed() - => _disposedList.Add(DateTimeOffset.UtcNow); - - private void SetLastEnabled(bool val) - => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val); - } - - private class IpcConfiguration - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber ModDirectoryChanged; - - private string _currentConfiguration = string.Empty; - private string _lastModDirectory = string.Empty; - private bool _lastModDirectoryValid; - private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; - - public IpcConfiguration(DalamudPluginInterface pi) - { - _pi = pi; - ModDirectoryChanged = Ipc.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Configuration"); - if (!_) - return; - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetModDirectory.Label, "Current Mod Directory"); - ImGui.TextUnformatted(Ipc.GetModDirectory.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.ModDirectoryChanged.Label, "Last Mod Directory Change"); - ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue - ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}" - : "None"); - DrawIntro(Ipc.GetConfiguration.Label, "Configuration"); - if (ImGui.Button("Get")) - { - _currentConfiguration = Ipc.GetConfiguration.Subscriber(_pi).Invoke(); - ImGui.OpenPopup("Config Popup"); - } - - DrawConfigPopup(); - } - - private void DrawConfigPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var popup = ImRaii.Popup("Config Popup"); - if (!popup) - return; - - using (ImRaii.PushFont(UiBuilder.MonoFont)) - { - ImGuiUtil.TextWrapped(_currentConfiguration); - } - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - - private void UpdateModDirectoryChanged(string path, bool valid) - => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now); - } - - private class Ui - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber PreSettingsDraw; - public readonly EventSubscriber PostSettingsDraw; - public readonly EventSubscriber Tooltip; - public readonly EventSubscriber Click; - - private string _lastDrawnMod = string.Empty; - private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; - private bool _subscribedToTooltip; - private bool _subscribedToClick; - private string _lastClicked = string.Empty; - private string _lastHovered = string.Empty; - private TabType _selectTab = TabType.None; - private string _modName = string.Empty; - private PenumbraApiEc _ec = PenumbraApiEc.Success; - - public Ui(DalamudPluginInterface pi) - { - _pi = pi; - PreSettingsDraw = Ipc.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); - PostSettingsDraw = Ipc.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); - Tooltip = Ipc.ChangedItemTooltip.Subscriber(pi, AddedTooltip); - Click = Ipc.ChangedItemClick.Subscriber(pi, AddedClick); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("UI"); - if (!_) - return; - - using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString())) - { - if (combo) - foreach (var val in Enum.GetValues()) - { - if (ImGui.Selectable(val.ToString(), _selectTab == val)) - _selectTab = val; - } - } - - ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.PostSettingsDraw.Label, "Last Drawn Mod"); - ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); - - DrawIntro(Ipc.ChangedItemTooltip.Label, "Add Tooltip"); - if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip)) - { - if (_subscribedToTooltip) - Tooltip.Enable(); - else - Tooltip.Disable(); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastHovered); - - DrawIntro(Ipc.ChangedItemClick.Label, "Subscribe Click"); - if (ImGui.Checkbox("##click", ref _subscribedToClick)) - { - if (_subscribedToClick) - Click.Enable(); - else - Click.Disable(); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastClicked); - DrawIntro(Ipc.OpenMainWindow.Label, "Open Mod Window"); - if (ImGui.Button("Open##window")) - _ec = Ipc.OpenMainWindow.Subscriber(_pi).Invoke(_selectTab, _modName, _modName); - - ImGui.SameLine(); - ImGui.TextUnformatted(_ec.ToString()); - - DrawIntro(Ipc.CloseMainWindow.Label, "Close Mod Window"); - if (ImGui.Button("Close##window")) - Ipc.CloseMainWindow.Subscriber(_pi).Invoke(); - } - - private void UpdateLastDrawnMod(string name) - => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); - - private void AddedTooltip(ChangedItemType type, uint id) - { - _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; - ImGui.TextUnformatted("IPC Test Successful"); - } - - private void AddedClick(MouseButton button, ChangedItemType type, uint id) - { - _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; - } - } - - private class Redrawing - { - private readonly DalamudPluginInterface _pi; - private readonly IClientState _clientState; - private readonly ObjectManager _objects; - public readonly EventSubscriber Redrawn; - - private string _redrawName = string.Empty; - private int _redrawIndex; - private string _lastRedrawnString = "None"; - - public Redrawing(DalamudPluginInterface pi, ObjectManager objects, IClientState clientState) - { - _pi = pi; - _objects = objects; - _clientState = clientState; - Redrawn = Ipc.GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Redrawing"); - if (!_) - return; - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.RedrawObjectByName.Label, "Redraw by Name"); - ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - ImGui.InputTextWithHint("##redrawName", "Name...", ref _redrawName, 32); - ImGui.SameLine(); - if (ImGui.Button("Redraw##Name")) - Ipc.RedrawObjectByName.Subscriber(_pi).Invoke(_redrawName, RedrawType.Redraw); - - DrawIntro(Ipc.RedrawObject.Label, "Redraw Player Character"); - if (ImGui.Button("Redraw##pc") && _clientState.LocalPlayer != null) - Ipc.RedrawObject.Subscriber(_pi).Invoke(_clientState.LocalPlayer, RedrawType.Redraw); - - DrawIntro(Ipc.RedrawObjectByIndex.Label, "Redraw by Index"); - var tmp = _redrawIndex; - ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount)) - _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount); - - ImGui.SameLine(); - if (ImGui.Button("Redraw##Index")) - Ipc.RedrawObjectByIndex.Subscriber(_pi).Invoke(_redrawIndex, RedrawType.Redraw); - - DrawIntro(Ipc.RedrawAll.Label, "Redraw All"); - if (ImGui.Button("Redraw##All")) - Ipc.RedrawAll.Subscriber(_pi).Invoke(RedrawType.Redraw); - - DrawIntro(Ipc.GameObjectRedrawn.Label, "Last Redrawn Object:"); - ImGui.TextUnformatted(_lastRedrawnString); - } - - private void SetLastRedrawn(IntPtr address, int index) - { - if (index < 0 - || index > _objects.TotalCount - || address == IntPtr.Zero - || _objects[index].Address != address) - _lastRedrawnString = "Invalid"; - - _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})"; - } - } - - private class GameState - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber CharacterBaseCreating; - public readonly EventSubscriber CharacterBaseCreated; - public readonly EventSubscriber GameObjectResourcePathResolved; - - - private string _lastCreatedGameObjectName = string.Empty; - private IntPtr _lastCreatedDrawObject = IntPtr.Zero; - private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; - private string _lastResolvedGamePath = string.Empty; - private string _lastResolvedFullPath = string.Empty; - private string _lastResolvedObject = string.Empty; - private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; - private string _currentDrawObjectString = string.Empty; - private IntPtr _currentDrawObject = IntPtr.Zero; - private int _currentCutsceneActor; - private int _currentCutsceneParent; - private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; - - public GameState(DalamudPluginInterface pi) - { - _pi = pi; - CharacterBaseCreating = Ipc.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); - CharacterBaseCreated = Ipc.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); - GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Game State"); - if (!_) - return; - - if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, - ImGuiInputTextFlags.CharsHexadecimal)) - _currentDrawObject = IntPtr.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, - out var tmp) - ? tmp - : IntPtr.Zero; - - ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); - ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0); - if (_cutsceneError is not PenumbraApiEc.Success) - { - ImGui.SameLine(); - ImGui.TextUnformatted("Invalid Argument on last Call"); - } - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetDrawObjectInfo.Label, "Draw Object Info"); - if (_currentDrawObject == IntPtr.Zero) - { - ImGui.TextUnformatted("Invalid"); - } - else - { - var (ptr, collection) = Ipc.GetDrawObjectInfo.Subscriber(_pi).Invoke(_currentDrawObject); - ImGui.TextUnformatted(ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}"); - } - - DrawIntro(Ipc.GetCutsceneParentIndex.Label, "Cutscene Parent"); - ImGui.TextUnformatted(Ipc.GetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor).ToString()); - - DrawIntro(Ipc.SetCutsceneParentIndex.Label, "Cutscene Parent"); - if (ImGui.Button("Set Parent")) - _cutsceneError = Ipc.SetCutsceneParentIndex.Subscriber(_pi).Invoke(_currentCutsceneActor, _currentCutsceneParent); - - DrawIntro(Ipc.CreatingCharacterBase.Label, "Last Drawobject created"); - if (_lastCreatedGameObjectTime < DateTimeOffset.Now) - ImGui.TextUnformatted(_lastCreatedDrawObject != IntPtr.Zero - ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" - : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"); - - DrawIntro(Ipc.GameObjectResourcePathResolved.Label, "Last GamePath resolved"); - if (_lastResolvedGamePathTime < DateTimeOffset.Now) - ImGui.TextUnformatted( - $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}"); - } - - private void UpdateLastCreated(IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4) - { - _lastCreatedGameObjectName = GetObjectName(gameObject); - _lastCreatedGameObjectTime = DateTimeOffset.Now; - _lastCreatedDrawObject = IntPtr.Zero; - } - - private void UpdateLastCreated2(IntPtr gameObject, string _, IntPtr drawObject) - { - _lastCreatedGameObjectName = GetObjectName(gameObject); - _lastCreatedGameObjectTime = DateTimeOffset.Now; - _lastCreatedDrawObject = drawObject; - } - - private void UpdateGameObjectResourcePath(IntPtr gameObject, string gamePath, string fullPath) - { - _lastResolvedObject = GetObjectName(gameObject); - _lastResolvedGamePath = gamePath; - _lastResolvedFullPath = fullPath; - _lastResolvedGamePathTime = DateTimeOffset.Now; - } - - private static unsafe string GetObjectName(IntPtr gameObject) - { - var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; - var name = obj != null ? obj->Name : null; - return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; - } - } - - private class Resolve(DalamudPluginInterface pi) - { - private string _currentResolvePath = string.Empty; - private string _currentResolveCharacter = string.Empty; - private string _currentReversePath = string.Empty; - private int _currentReverseIdx; - private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], [])); - - public void Draw() - { - using var tree = ImRaii.TreeNode("Resolving"); - if (!tree) - return; - - ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); - ImGui.InputTextWithHint("##resolveCharacter", "Character Name (leave blank for default)...", ref _currentResolveCharacter, 32); - ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, - Utf8GamePath.MaxGamePathLength); - ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.ResolveDefaultPath.Label, "Default Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveDefaultPath.Subscriber(pi).Invoke(_currentResolvePath)); - - DrawIntro(Ipc.ResolveInterfacePath.Label, "Interface Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveInterfacePath.Subscriber(pi).Invoke(_currentResolvePath)); - - DrawIntro(Ipc.ResolvePlayerPath.Label, "Player Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolvePlayerPath.Subscriber(pi).Invoke(_currentResolvePath)); - - DrawIntro(Ipc.ResolveCharacterPath.Label, "Character Collection Resolve"); - if (_currentResolvePath.Length != 0 && _currentResolveCharacter.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveCharacterPath.Subscriber(pi).Invoke(_currentResolvePath, _currentResolveCharacter)); - - DrawIntro(Ipc.ResolveGameObjectPath.Label, "Game Object Collection Resolve"); - if (_currentResolvePath.Length != 0) - ImGui.TextUnformatted(Ipc.ResolveGameObjectPath.Subscriber(pi).Invoke(_currentResolvePath, _currentReverseIdx)); - - DrawIntro(Ipc.ReverseResolvePath.Label, "Reversed Game Paths"); - if (_currentReversePath.Length > 0) - { - var list = Ipc.ReverseResolvePath.Subscriber(pi).Invoke(_currentReversePath, _currentResolveCharacter); - if (list.Length > 0) - { - ImGui.TextUnformatted(list[0]); - if (list.Length > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", list.Skip(1))); - } - } - - DrawIntro(Ipc.ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); - if (_currentReversePath.Length > 0) - { - var list = Ipc.ReverseResolvePlayerPath.Subscriber(pi).Invoke(_currentReversePath); - if (list.Length > 0) - { - ImGui.TextUnformatted(list[0]); - if (list.Length > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", list.Skip(1))); - } - } - - DrawIntro(Ipc.ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); - if (_currentReversePath.Length > 0) - { - var list = Ipc.ReverseResolveGameObjectPath.Subscriber(pi).Invoke(_currentReversePath, _currentReverseIdx); - if (list.Length > 0) - { - ImGui.TextUnformatted(list[0]); - if (list.Length > 1 && ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join("\n", list.Skip(1))); - } - } - - var forwardArray = _currentResolvePath.Length > 0 - ? [_currentResolvePath] - : Array.Empty(); - var reverseArray = _currentReversePath.Length > 0 - ? [_currentReversePath] - : Array.Empty(); - - DrawIntro(Ipc.ResolvePlayerPaths.Label, "Resolved Paths (Player)"); - if (forwardArray.Length > 0 || reverseArray.Length > 0) - { - var ret = Ipc.ResolvePlayerPaths.Subscriber(pi).Invoke(forwardArray, reverseArray); - ImGui.TextUnformatted(ConvertText(ret)); - } - - DrawIntro(Ipc.ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); - if (ImGui.Button("Start")) - _task = Ipc.ResolvePlayerPathsAsync.Subscriber(pi).Invoke(forwardArray, reverseArray); - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(_task.Status.ToString()); - if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) - ImGui.SetTooltip(ConvertText(_task.Result)); - return; - - static string ConvertText((string[], string[][]) data) - { - var text = string.Empty; - if (data.Item1.Length > 0) - { - if (data.Item2.Length > 0) - text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}."; - else - text = $"Forward: {data.Item1[0]}."; - } - else if (data.Item2.Length > 0) - { - text = $"Reverse: {string.Join("; ", data.Item2[0])}."; - } - - return text; - } - } - } - - private class Collections - { - private readonly DalamudPluginInterface _pi; - - private int _objectIdx; - private string _collectionName = string.Empty; - private bool _allowCreation = true; - private bool _allowDeletion = true; - private ApiCollectionType _type = ApiCollectionType.Current; - - private string _characterCollectionName = string.Empty; - private IList _collections = []; - private string _changedItemCollection = string.Empty; - private IReadOnlyDictionary _changedItems = new Dictionary(); - private PenumbraApiEc _returnCode = PenumbraApiEc.Success; - private string? _oldCollection; - - public Collections(DalamudPluginInterface pi) - => _pi = pi; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Collections"); - if (!_) - return; - - ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); - ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); - ImGui.InputText("Collection Name##Collections", ref _collectionName, 64); - ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); - ImGui.SameLine(); - ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro("Last Return Code", _returnCode.ToString()); - if (_oldCollection != null) - ImGui.TextUnformatted(_oldCollection.Length == 0 ? "Created" : _oldCollection); - - DrawIntro(Ipc.GetCurrentCollectionName.Label, "Current Collection"); - ImGui.TextUnformatted(Ipc.GetCurrentCollectionName.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.GetDefaultCollectionName.Label, "Default Collection"); - ImGui.TextUnformatted(Ipc.GetDefaultCollectionName.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.GetInterfaceCollectionName.Label, "Interface Collection"); - ImGui.TextUnformatted(Ipc.GetInterfaceCollectionName.Subscriber(_pi).Invoke()); - DrawIntro(Ipc.GetCharacterCollectionName.Label, "Character"); - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - ImGui.InputTextWithHint("##characterCollectionName", "Character Name...", ref _characterCollectionName, 64); - var (c, s) = Ipc.GetCharacterCollectionName.Subscriber(_pi).Invoke(_characterCollectionName); - ImGui.SameLine(); - ImGui.TextUnformatted($"{c}, {(s ? "Custom" : "Default")}"); - - DrawIntro(Ipc.GetCollections.Label, "Collections"); - if (ImGui.Button("Get##Collections")) - { - _collections = Ipc.GetCollections.Subscriber(_pi).Invoke(); - ImGui.OpenPopup("Collections"); - } - - DrawIntro(Ipc.GetCollectionForType.Label, "Get Special Collection"); - var name = Ipc.GetCollectionForType.Subscriber(_pi).Invoke(_type); - ImGui.TextUnformatted(name.Length == 0 ? "Unassigned" : name); - DrawIntro(Ipc.SetCollectionForType.Label, "Set Special Collection"); - if (ImGui.Button("Set##TypeCollection")) - (_returnCode, _oldCollection) = - Ipc.SetCollectionForType.Subscriber(_pi).Invoke(_type, _collectionName, _allowCreation, _allowDeletion); - - DrawIntro(Ipc.GetCollectionForObject.Label, "Get Object Collection"); - (var valid, var individual, name) = Ipc.GetCollectionForObject.Subscriber(_pi).Invoke(_objectIdx); - ImGui.TextUnformatted( - $"{(valid ? "Valid" : "Invalid")} Object, {(name.Length == 0 ? "Unassigned" : name)}{(individual ? " (Individual Assignment)" : string.Empty)}"); - DrawIntro(Ipc.SetCollectionForObject.Label, "Set Object Collection"); - if (ImGui.Button("Set##ObjectCollection")) - (_returnCode, _oldCollection) = Ipc.SetCollectionForObject.Subscriber(_pi) - .Invoke(_objectIdx, _collectionName, _allowCreation, _allowDeletion); - - if (_returnCode == PenumbraApiEc.NothingChanged && _oldCollection.IsNullOrEmpty()) - _oldCollection = null; - - DrawIntro(Ipc.GetChangedItems.Label, "Changed Item List"); - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - ImGui.InputTextWithHint("##changedCollection", "Collection Name...", ref _changedItemCollection, 64); - ImGui.SameLine(); - if (ImGui.Button("Get")) - { - _changedItems = Ipc.GetChangedItems.Subscriber(_pi).Invoke(_changedItemCollection); - ImGui.OpenPopup("Changed Item List"); - } - - DrawChangedItemPopup(); - DrawCollectionPopup(); - } - - private void DrawChangedItemPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var p = ImRaii.Popup("Changed Item List"); - if (!p) - return; - - foreach (var item in _changedItems) - ImGui.TextUnformatted(item.Key); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - - private void DrawCollectionPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var p = ImRaii.Popup("Collections"); - if (!p) - return; - - foreach (var collection in _collections) - ImGui.TextUnformatted(collection); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - } - - private class Meta - { - private readonly DalamudPluginInterface _pi; - - private string _characterName = string.Empty; - private int _gameObjectIndex; - - public Meta(DalamudPluginInterface pi) - => _pi = pi; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Meta"); - if (!_) - return; - - ImGui.InputTextWithHint("##characterName", "Character Name...", ref _characterName, 64); - ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetMetaManipulations.Label, "Meta Manipulations"); - if (ImGui.Button("Copy to Clipboard")) - { - var base64 = Ipc.GetMetaManipulations.Subscriber(_pi).Invoke(_characterName); - ImGui.SetClipboardText(base64); - } - - DrawIntro(Ipc.GetPlayerMetaManipulations.Label, "Player Meta Manipulations"); - if (ImGui.Button("Copy to Clipboard##Player")) - { - var base64 = Ipc.GetPlayerMetaManipulations.Subscriber(_pi).Invoke(); - ImGui.SetClipboardText(base64); - } - - DrawIntro(Ipc.GetGameObjectMetaManipulations.Label, "Game Object Manipulations"); - if (ImGui.Button("Copy to Clipboard##GameObject")) - { - var base64 = Ipc.GetGameObjectMetaManipulations.Subscriber(_pi).Invoke(_gameObjectIndex); - ImGui.SetClipboardText(base64); - } - } - } - - private class Mods - { - private readonly DalamudPluginInterface _pi; - - private string _modDirectory = string.Empty; - private string _modName = string.Empty; - private string _pathInput = string.Empty; - private string _newInstallPath = string.Empty; - private PenumbraApiEc _lastReloadEc; - private PenumbraApiEc _lastAddEc; - private PenumbraApiEc _lastDeleteEc; - private PenumbraApiEc _lastSetPathEc; - private PenumbraApiEc _lastInstallEc; - private IList<(string, string)> _mods = new List<(string, string)>(); - - public readonly EventSubscriber DeleteSubscriber; - public readonly EventSubscriber AddSubscriber; - public readonly EventSubscriber MoveSubscriber; - - private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch; - private string _lastDeletedMod = string.Empty; - private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch; - private string _lastAddedMod = string.Empty; - private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch; - private string _lastMovedModFrom = string.Empty; - private string _lastMovedModTo = string.Empty; - - public Mods(DalamudPluginInterface pi) - { - _pi = pi; - DeleteSubscriber = Ipc.ModDeleted.Subscriber(pi, s => - { - _lastDeletedModTime = DateTimeOffset.UtcNow; - _lastDeletedMod = s; - }); - AddSubscriber = Ipc.ModAdded.Subscriber(pi, s => - { - _lastAddedModTime = DateTimeOffset.UtcNow; - _lastAddedMod = s; - }); - MoveSubscriber = Ipc.ModMoved.Subscriber(pi, (s1, s2) => - { - _lastMovedModTime = DateTimeOffset.UtcNow; - _lastMovedModFrom = s1; - _lastMovedModTo = s2; - }); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Mods"); - if (!_) - return; - - ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100); - ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); - ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); - ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetMods.Label, "Mods"); - if (ImGui.Button("Get##Mods")) - { - _mods = Ipc.GetMods.Subscriber(_pi).Invoke(); - ImGui.OpenPopup("Mods"); - } - - DrawIntro(Ipc.ReloadMod.Label, "Reload Mod"); - if (ImGui.Button("Reload")) - _lastReloadEc = Ipc.ReloadMod.Subscriber(_pi).Invoke(_modDirectory, _modName); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastReloadEc.ToString()); - - DrawIntro(Ipc.InstallMod.Label, "Install Mod"); - if (ImGui.Button("Install")) - _lastInstallEc = Ipc.InstallMod.Subscriber(_pi).Invoke(_newInstallPath); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastInstallEc.ToString()); - - DrawIntro(Ipc.AddMod.Label, "Add Mod"); - if (ImGui.Button("Add")) - _lastAddEc = Ipc.AddMod.Subscriber(_pi).Invoke(_modDirectory); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastAddEc.ToString()); - - DrawIntro(Ipc.DeleteMod.Label, "Delete Mod"); - if (ImGui.Button("Delete")) - _lastDeleteEc = Ipc.DeleteMod.Subscriber(_pi).Invoke(_modDirectory, _modName); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastDeleteEc.ToString()); - - DrawIntro(Ipc.GetModPath.Label, "Current Path"); - var (ec, path, def) = Ipc.GetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName); - ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")}) [{ec}]"); - - DrawIntro(Ipc.SetModPath.Label, "Set Path"); - if (ImGui.Button("Set")) - _lastSetPathEc = Ipc.SetModPath.Subscriber(_pi).Invoke(_modDirectory, _modName, _pathInput); - - ImGui.SameLine(); - ImGui.TextUnformatted(_lastSetPathEc.ToString()); - - DrawIntro(Ipc.ModDeleted.Label, "Last Mod Deleted"); - if (_lastDeletedModTime > DateTimeOffset.UnixEpoch) - ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}"); - - DrawIntro(Ipc.ModAdded.Label, "Last Mod Added"); - if (_lastAddedModTime > DateTimeOffset.UnixEpoch) - ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}"); - - DrawIntro(Ipc.ModMoved.Label, "Last Mod Moved"); - if (_lastMovedModTime > DateTimeOffset.UnixEpoch) - ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}"); - - DrawModsPopup(); - } - - private void DrawModsPopup() - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); - using var p = ImRaii.Popup("Mods"); - if (!p) - return; - - foreach (var (modDir, modName) in _mods) - ImGui.TextUnformatted($"{modDir}: {modName}"); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - ImGui.CloseCurrentPopup(); - } - } - - private class ModSettings - { - private readonly DalamudPluginInterface _pi; - public readonly EventSubscriber SettingChanged; - - private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; - private ModSettingChange _lastSettingChangeType; - private string _lastSettingChangeCollection = string.Empty; - private string _lastSettingChangeMod = string.Empty; - private bool _lastSettingChangeInherited; - private DateTimeOffset _lastSettingChange; - - private string _settingsModDirectory = string.Empty; - private string _settingsModName = string.Empty; - private string _settingsCollection = string.Empty; - private bool _settingsAllowInheritance = true; - private bool _settingsInherit; - private bool _settingsEnabled; - private int _settingsPriority; - private IDictionary, GroupType)>? _availableSettings; - private IDictionary>? _currentSettings; - - public ModSettings(DalamudPluginInterface pi) - { - _pi = pi; - SettingChanged = Ipc.ModSettingChanged.Subscriber(pi, UpdateLastModSetting); - } - - public void Draw() - { - using var _ = ImRaii.TreeNode("Mod Settings"); - if (!_) - return; - - ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); - ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); - ImGui.InputTextWithHint("##settingsCollection", "Collection...", ref _settingsCollection, 100); - ImGui.Checkbox("Allow Inheritance", ref _settingsAllowInheritance); - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro("Last Error", _lastSettingsError.ToString()); - DrawIntro(Ipc.ModSettingChanged.Label, "Last Mod Setting Changed"); - ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0 - ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}" - : "None"); - DrawIntro(Ipc.GetAvailableModSettings.Label, "Get Available Settings"); - if (ImGui.Button("Get##Available")) - { - _availableSettings = Ipc.GetAvailableModSettings.Subscriber(_pi).Invoke(_settingsModDirectory, _settingsModName); - _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; - } - - - DrawIntro(Ipc.GetCurrentModSettings.Label, "Get Current Settings"); - if (ImGui.Button("Get##Current")) - { - var ret = Ipc.GetCurrentModSettings.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsAllowInheritance); - _lastSettingsError = ret.Item1; - if (ret.Item1 == PenumbraApiEc.Success) - { - _settingsEnabled = ret.Item2?.Item1 ?? false; - _settingsInherit = ret.Item2?.Item4 ?? false; - _settingsPriority = ret.Item2?.Item2 ?? 0; - _currentSettings = ret.Item2?.Item3; - } - else - { - _currentSettings = null; - } - } - - DrawIntro(Ipc.TryInheritMod.Label, "Inherit Mod"); - ImGui.Checkbox("##inherit", ref _settingsInherit); - ImGui.SameLine(); - if (ImGui.Button("Set##Inherit")) - _lastSettingsError = Ipc.TryInheritMod.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsInherit); - - DrawIntro(Ipc.TrySetMod.Label, "Set Enabled"); - ImGui.Checkbox("##enabled", ref _settingsEnabled); - ImGui.SameLine(); - if (ImGui.Button("Set##Enabled")) - _lastSettingsError = Ipc.TrySetMod.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsEnabled); - - DrawIntro(Ipc.TrySetModPriority.Label, "Set Priority"); - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - ImGui.DragInt("##Priority", ref _settingsPriority); - ImGui.SameLine(); - if (ImGui.Button("Set##Priority")) - _lastSettingsError = Ipc.TrySetModPriority.Subscriber(_pi) - .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName, _settingsPriority); - - DrawIntro(Ipc.CopyModSettings.Label, "Copy Mod Settings"); - if (ImGui.Button("Copy Settings")) - _lastSettingsError = Ipc.CopyModSettings.Subscriber(_pi).Invoke(_settingsCollection, _settingsModDirectory, _settingsModName); - - ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection."); - - DrawIntro(Ipc.TrySetModSetting.Label, "Set Setting(s)"); - if (_availableSettings == null) - return; - - foreach (var (group, (list, type)) in _availableSettings) - { - using var id = ImRaii.PushId(group); - var preview = list.Count > 0 ? list[0] : string.Empty; - IList current; - if (_currentSettings != null && _currentSettings.TryGetValue(group, out current!) && current.Count > 0) - { - preview = current[0]; - } - else - { - current = new List(); - if (_currentSettings != null) - _currentSettings[group] = current; - } - - ImGui.SetNextItemWidth(200 * UiHelpers.Scale); - using (var c = ImRaii.Combo("##group", preview)) - { - if (c) - foreach (var s in list) - { - var contained = current.Contains(s); - if (ImGui.Checkbox(s, ref contained)) - { - if (contained) - current.Add(s); - else - current.Remove(s); - } - } - } - - ImGui.SameLine(); - if (ImGui.Button("Set##setting")) - { - if (type == GroupType.Single) - _lastSettingsError = Ipc.TrySetModSetting.Subscriber(_pi).Invoke(_settingsCollection, - _settingsModDirectory, _settingsModName, group, current.Count > 0 ? current[0] : string.Empty); - else - _lastSettingsError = Ipc.TrySetModSettings.Subscriber(_pi).Invoke(_settingsCollection, - _settingsModDirectory, _settingsModName, group, current.ToArray()); - } - - ImGui.SameLine(); - ImGui.TextUnformatted(group); - } - } - - private void UpdateLastModSetting(ModSettingChange type, string collection, string mod, bool inherited) - { - _lastSettingChangeType = type; - _lastSettingChangeCollection = collection; - _lastSettingChangeMod = mod; - _lastSettingChangeInherited = inherited; - _lastSettingChange = DateTimeOffset.Now; - } - } - - private class Editing - { - private readonly DalamudPluginInterface _pi; - - private string _inputPath = string.Empty; - private string _inputPath2 = string.Empty; - private string _outputPath = string.Empty; - private string _outputPath2 = string.Empty; - - private TextureType _typeSelector; - private bool _mipMaps = true; - - private Task? _task1; - private Task? _task2; - - public Editing(DalamudPluginInterface pi) - => _pi = pi; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Editing"); - if (!_) - return; - - ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256); - ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256); - ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256); - ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256); - TypeCombo(); - ImGui.Checkbox("Add MipMaps", ref _mipMaps); - - using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 1"); - if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false })) - _task1 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps); - ImGui.SameLine(); - ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString()); - if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted) - ImGui.SetTooltip(_task1.Exception?.ToString()); - - DrawIntro(Ipc.ConvertTextureFile.Label, "Convert Texture 2"); - if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false })) - _task2 = Ipc.ConvertTextureFile.Subscriber(_pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps); - ImGui.SameLine(); - ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString()); - if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted) - ImGui.SetTooltip(_task2.Exception?.ToString()); - } - - private void TypeCombo() - { - using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString()); - if (!combo) - return; - - foreach (var value in Enum.GetValues()) - { - if (ImGui.Selectable(value.ToString(), _typeSelector == value)) - _typeSelector = value; - } - } - } - - private class Temporary - { - private readonly DalamudPluginInterface _pi; - private readonly ModManager _modManager; - private readonly CollectionManager _collections; - private readonly TempModManager _tempMods; - private readonly TempCollectionManager _tempCollections; - private readonly SaveService _saveService; - private readonly Configuration _config; - - public Temporary(DalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods, - TempCollectionManager tempCollections, SaveService saveService, Configuration config) - { - _pi = pi; - _modManager = modManager; - _collections = collections; - _tempMods = tempMods; - _tempCollections = tempCollections; - _saveService = saveService; - _config = config; - } - - public string LastCreatedCollectionName = string.Empty; - - private string _tempCollectionName = string.Empty; - private string _tempCharacterName = string.Empty; - private string _tempModName = string.Empty; - private string _tempGamePath = "test/game/path.mtrl"; - private string _tempFilePath = "test/success.mtrl"; - private string _tempManipulation = string.Empty; - private PenumbraApiEc _lastTempError; - private int _tempActorIndex; - private bool _forceOverwrite; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Temporary"); - if (!_) - return; - - ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128); - ImGui.InputTextWithHint("##tempCollectionChar", "Collection Character...", ref _tempCharacterName, 32); - ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); - ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); - ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); - ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); - ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); - - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro("Last Error", _lastTempError.ToString()); - DrawIntro("Last Created Collection", LastCreatedCollectionName); - DrawIntro(Ipc.CreateTemporaryCollection.Label, "Create Temporary Collection"); -#pragma warning disable 0612 - if (ImGui.Button("Create##Collection")) - (_lastTempError, LastCreatedCollectionName) = Ipc.CreateTemporaryCollection.Subscriber(_pi) - .Invoke(_tempCollectionName, _tempCharacterName, _forceOverwrite); - - DrawIntro(Ipc.CreateNamedTemporaryCollection.Label, "Create Named Temporary Collection"); - if (ImGui.Button("Create##NamedCollection")) - _lastTempError = Ipc.CreateNamedTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName); - - DrawIntro(Ipc.RemoveTemporaryCollection.Label, "Remove Temporary Collection from Character"); - if (ImGui.Button("Delete##Collection")) - _lastTempError = Ipc.RemoveTemporaryCollection.Subscriber(_pi).Invoke(_tempCharacterName); -#pragma warning restore 0612 - DrawIntro(Ipc.RemoveTemporaryCollectionByName.Label, "Remove Temporary Collection"); - if (ImGui.Button("Delete##NamedCollection")) - _lastTempError = Ipc.RemoveTemporaryCollectionByName.Subscriber(_pi).Invoke(_tempCollectionName); - - DrawIntro(Ipc.AssignTemporaryCollection.Label, "Assign Temporary Collection"); - if (ImGui.Button("Assign##NamedCollection")) - _lastTempError = Ipc.AssignTemporaryCollection.Subscriber(_pi).Invoke(_tempCollectionName, _tempActorIndex, _forceOverwrite); - - DrawIntro(Ipc.AddTemporaryMod.Label, "Add Temporary Mod to specific Collection"); - if (ImGui.Button("Add##Mod")) - _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, - new Dictionary { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); - - DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection"); - if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, - "Copies the effective list from the collection named in Temporary Mod Name...", - !_collections.Storage.ByName(_tempModName, out var copyCollection)) - && copyCollection is { HasCache: true }) - { - var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); - var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), - MetaManipulation.CurrentVersion); - _lastTempError = Ipc.AddTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, files, manips, 999); - } - - DrawIntro(Ipc.AddTemporaryModAll.Label, "Add Temporary Mod to all Collections"); - if (ImGui.Button("Add##All")) - _lastTempError = Ipc.AddTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, - new Dictionary { { _tempGamePath, _tempFilePath } }, - _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); - - DrawIntro(Ipc.RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection"); - if (ImGui.Button("Remove##Mod")) - _lastTempError = Ipc.RemoveTemporaryMod.Subscriber(_pi).Invoke(_tempModName, _tempCollectionName, int.MaxValue); - - DrawIntro(Ipc.RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); - if (ImGui.Button("Remove##ModAll")) - _lastTempError = Ipc.RemoveTemporaryModAll.Subscriber(_pi).Invoke(_tempModName, int.MaxValue); - } - - public void DrawCollections() - { - using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); - if (!collTree) - return; - - using var table = ImRaii.Table("##collTree", 5); - if (!table) - return; - - foreach (var collection in _tempCollections.Values) - { - ImGui.TableNextColumn(); - var character = _tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) - .FirstOrDefault() - ?? "Unknown"; - if (ImGui.Button($"Save##{collection.Name}")) - TemporaryMod.SaveTempCollection(_config, _saveService, _modManager, collection, character); - - ImGuiUtil.DrawTableColumn(collection.Name); - ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); - ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); - ImGuiUtil.DrawTableColumn(string.Join(", ", - _tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); - } - } - - public void DrawMods() - { - using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods"); - if (!modTree) - return; - - using var table = ImRaii.Table("##modTree", 5); - - void PrintList(string collectionName, IReadOnlyList list) - { - foreach (var mod in list) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Name); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Priority.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(collectionName); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.Default.Files.Count.ToString()); - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - foreach (var (path, file) in mod.Default.Files) - ImGui.TextUnformatted($"{path} -> {file}"); - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(mod.TotalManipulations.ToString()); - if (ImGui.IsItemHovered()) - { - using var tt = ImRaii.Tooltip(); - foreach (var manip in mod.Default.Manipulations) - ImGui.TextUnformatted(manip.ToString()); - } - } - } - - if (table) - { - PrintList("All", _tempMods.ModsForAllCollections); - foreach (var (collection, list) in _tempMods.Mods) - PrintList(collection.Name, list); - } - } - } - - private class ResourceTree(DalamudPluginInterface pi, ObjectManager objects) - { - private readonly Stopwatch _stopwatch = new(); - - private string _gameObjectIndices = "0"; - private ResourceType _type = ResourceType.Mtrl; - private bool _withUiData; - - private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcePaths; - private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcePaths; - private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; - private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; - private (string, Ipc.ResourceTree?)[]? _lastGameObjectResourceTrees; - private (string, Ipc.ResourceTree)[]? _lastPlayerResourceTrees; - private TimeSpan _lastCallDuration; - - public void Draw() - { - using var _ = ImRaii.TreeNode("Resource Tree"); - if (!_) - return; - - ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); - ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); - ImGui.Checkbox("Also get names and icons", ref _withUiData); - - using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - DrawIntro(Ipc.GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); - if (ImGui.Button("Get##GameObjectResourcePaths")) - { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcePaths.Subscriber(pi); - _stopwatch.Restart(); - var resourcePaths = subscriber.Invoke(gameObjects); - - _lastCallDuration = _stopwatch.Elapsed; - _lastGameObjectResourcePaths = gameObjects - .Select(i => GameObjectToString(i)) - .Zip(resourcePaths) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcePaths)); - } - - DrawIntro(Ipc.GetPlayerResourcePaths.Label, "Get local player resource paths"); - if (ImGui.Button("Get##PlayerResourcePaths")) - { - var subscriber = Ipc.GetPlayerResourcePaths.Subscriber(pi); - _stopwatch.Restart(); - var resourcePaths = subscriber.Invoke(); - - _lastCallDuration = _stopwatch.Elapsed; - _lastPlayerResourcePaths = resourcePaths - .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcePaths)); - } - - DrawIntro(Ipc.GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); - if (ImGui.Button("Get##GameObjectResourcesOfType")) - { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourcesOfType.Subscriber(pi); - _stopwatch.Restart(); - var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects); - - _lastCallDuration = _stopwatch.Elapsed; - _lastGameObjectResourcesOfType = gameObjects - .Select(i => GameObjectToString(i)) - .Zip(resourcesOfType) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourcesOfType)); - } - - DrawIntro(Ipc.GetPlayerResourcesOfType.Label, "Get local player resources of type"); - if (ImGui.Button("Get##PlayerResourcesOfType")) - { - var subscriber = Ipc.GetPlayerResourcesOfType.Subscriber(pi); - _stopwatch.Restart(); - var resourcesOfType = subscriber.Invoke(_type, _withUiData); - - _lastCallDuration = _stopwatch.Elapsed; - _lastPlayerResourcesOfType = resourcesOfType - .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetPlayerResourcesOfType)); - } - - DrawIntro(Ipc.GetGameObjectResourceTrees.Label, "Get GameObject resource trees"); - if (ImGui.Button("Get##GameObjectResourceTrees")) - { - var gameObjects = GetSelectedGameObjects(); - var subscriber = Ipc.GetGameObjectResourceTrees.Subscriber(pi); - _stopwatch.Restart(); - var trees = subscriber.Invoke(_withUiData, gameObjects); - - _lastCallDuration = _stopwatch.Elapsed; - _lastGameObjectResourceTrees = gameObjects - .Select(i => GameObjectToString(i)) - .Zip(trees) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetGameObjectResourceTrees)); - } - - DrawIntro(Ipc.GetPlayerResourceTrees.Label, "Get local player resource trees"); - if (ImGui.Button("Get##PlayerResourceTrees")) - { - var subscriber = Ipc.GetPlayerResourceTrees.Subscriber(pi); - _stopwatch.Restart(); - var trees = subscriber.Invoke(_withUiData); - - _lastCallDuration = _stopwatch.Elapsed; - _lastPlayerResourceTrees = trees - .Select(pair => (GameObjectToString(pair.Key), pair.Value)) - .ToArray(); - - ImGui.OpenPopup(nameof(Ipc.GetPlayerResourceTrees)); - } - - DrawPopup(nameof(Ipc.GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourcePaths), ref _lastPlayerResourcePaths, DrawResourcePaths, _lastCallDuration); - - DrawPopup(nameof(Ipc.GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, _lastCallDuration); - - DrawPopup(nameof(Ipc.GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, _lastCallDuration); - DrawPopup(nameof(Ipc.GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); - } - - private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class - { - ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); - using var popup = ImRaii.Popup(popupId); - if (!popup) - { - result = null; - return; - } - - if (result == null) - { - ImGui.CloseCurrentPopup(); - return; - } - - drawResult(result); - - ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms"); - - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) - { - result = null; - ImGui.CloseCurrentPopup(); - } - } - - private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class - { - var firstSeen = new Dictionary(); - foreach (var (label, item) in result) - { - if (item == null) - { - ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose(); - continue; - } - - if (firstSeen.TryGetValue(item, out var firstLabel)) - { - ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose(); - continue; - } - - firstSeen.Add(item, label); - - using var header = ImRaii.TreeNode(label); - if (!header) - continue; - - drawItem(item); - } - } - - private static void DrawResourcePaths((string, IReadOnlyDictionary?)[] result) - { - DrawWithHeaders(result, paths => - { - using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); - ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); - ImGui.TableHeadersRow(); - - foreach (var (actualPath, gamePaths) in paths) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(actualPath); - ImGui.TableNextColumn(); - foreach (var gamePath in gamePaths) - ImGui.TextUnformatted(gamePath); - } - }); - } - - private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result) - { - DrawWithHeaders(result, resources => - { - using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f); - if (_withUiData) - ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); - ImGui.TableHeadersRow(); - - foreach (var (resourceHandle, (actualPath, name, icon)) in resources) - { - ImGui.TableNextColumn(); - TextUnformattedMono($"0x{resourceHandle:X}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(actualPath); - if (_withUiData) - { - ImGui.TableNextColumn(); - TextUnformattedMono(icon.ToString()); - ImGui.SameLine(); - ImGui.TextUnformatted(name); - } - } - }); - } - - private void DrawResourceTrees((string, Ipc.ResourceTree?)[] result) - { - DrawWithHeaders(result, tree => - { - ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); - - using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); - if (!table) - return; - - if (_withUiData) - { - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); - ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f); - } - else - { - ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); - } - - ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); - ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImGui.TableHeadersRow(); - - void DrawNode(Ipc.ResourceNode node) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - var hasChildren = node.Children.Any(); - using var treeNode = ImRaii.TreeNode( - $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", - hasChildren - ? ImGuiTreeNodeFlags.SpanFullWidth - : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen); - if (_withUiData) - { - ImGui.TableNextColumn(); - TextUnformattedMono(node.Type.ToString()); - ImGui.TableNextColumn(); - TextUnformattedMono(node.Icon.ToString()); - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(node.GamePath ?? "Unknown"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(node.ActualPath); - ImGui.TableNextColumn(); - TextUnformattedMono($"0x{node.ObjectAddress:X8}"); - ImGui.TableNextColumn(); - TextUnformattedMono($"0x{node.ResourceHandle:X8}"); - - if (treeNode) - foreach (var child in node.Children) - DrawNode(child); - } - - foreach (var node in tree.Nodes) - DrawNode(node); - }); - } - - private static void TextUnformattedMono(string text) - { - using var _ = ImRaii.PushFont(UiBuilder.MonoFont); - ImGui.TextUnformatted(text); - } - - private ushort[] GetSelectedGameObjects() - => _gameObjectIndices.Split(',') - .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) - .ToArray(); - - private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) - { - var gameObject = objects[gameObjectIndex]; - - return gameObject.Valid - ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})" - : $"[{gameObjectIndex}] null"; - } - } -} diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs new file mode 100644 index 00000000..12314f0c --- /dev/null +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -0,0 +1,166 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Enums; +using ImGuiClip = OtterGui.ImGuiClip; + +namespace Penumbra.Api.IpcTester; + +public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService +{ + private int _objectIdx; + private string _collectionIdString = string.Empty; + private Guid? _collectionId = null; + private bool _allowCreation = true; + private bool _allowDeletion = true; + private ApiCollectionType _type = ApiCollectionType.Yourself; + + private Dictionary _collections = []; + private (string, ChangedItemType, uint)[] _changedItems = []; + private PenumbraApiEc _returnCode = PenumbraApiEc.Success; + private (Guid Id, string Name)? _oldCollection; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Collections"); + if (!_) + return; + + ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); + ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); + ImGuiUtil.GuidInput("Collection Id##Collections", "Collection GUID...", string.Empty, ref _collectionId, ref _collectionIdString); + ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); + ImGui.SameLine(); + ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); + + using var table = ImRaii.Table(string.Empty, 4, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro("Last Return Code", _returnCode.ToString()); + if (_oldCollection != null) + ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString()); + + IpcTester.DrawIntro(GetCollection.Label, "Current Collection"); + DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current)); + + IpcTester.DrawIntro(GetCollection.Label, "Default Collection"); + DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Default)); + + IpcTester.DrawIntro(GetCollection.Label, "Interface Collection"); + DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Interface)); + + IpcTester.DrawIntro(GetCollection.Label, "Special Collection"); + DrawCollection(new GetCollection(pi).Invoke(_type)); + + IpcTester.DrawIntro(GetCollections.Label, "Collections"); + DrawCollectionPopup(); + if (ImGui.Button("Get##Collections")) + { + _collections = new GetCollections(pi).Invoke(); + ImGui.OpenPopup("Collections"); + } + + IpcTester.DrawIntro(GetCollectionForObject.Label, "Get Object Collection"); + var (valid, individual, effectiveCollection) = new GetCollectionForObject(pi).Invoke(_objectIdx); + DrawCollection(effectiveCollection); + ImGui.SameLine(); + ImGui.TextUnformatted($"({(valid ? "Valid" : "Invalid")} Object{(individual ? ", Individual Assignment)" : ")")}"); + + IpcTester.DrawIntro(SetCollection.Label, "Set Special Collection"); + if (ImGui.Button("Set##SpecialCollection")) + (_returnCode, _oldCollection) = + new SetCollection(pi).Invoke(_type, _collectionId.GetValueOrDefault(Guid.Empty), _allowCreation, _allowDeletion); + ImGui.TableNextColumn(); + if (ImGui.Button("Remove##SpecialCollection")) + (_returnCode, _oldCollection) = new SetCollection(pi).Invoke(_type, null, _allowCreation, _allowDeletion); + + IpcTester.DrawIntro(SetCollectionForObject.Label, "Set Object Collection"); + if (ImGui.Button("Set##ObjectCollection")) + (_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, _collectionId.GetValueOrDefault(Guid.Empty), + _allowCreation, _allowDeletion); + ImGui.TableNextColumn(); + if (ImGui.Button("Remove##ObjectCollection")) + (_returnCode, _oldCollection) = new SetCollectionForObject(pi).Invoke(_objectIdx, null, _allowCreation, _allowDeletion); + + IpcTester.DrawIntro(GetChangedItemsForCollection.Label, "Changed Item List"); + DrawChangedItemPopup(); + if (ImGui.Button("Get##ChangedItems")) + { + var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty)); + _changedItems = items.Select(kvp => + { + var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value); + return (kvp.Key, type, id); + }).ToArray(); + ImGui.OpenPopup("Changed Item List"); + } + } + + private void DrawChangedItemPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Changed Item List"); + if (!p) + return; + + using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) + { + if (t) + ImGuiClip.ClippedDraw(_changedItems, t => + { + ImGuiUtil.DrawTableColumn(t.Item1); + ImGuiUtil.DrawTableColumn(t.Item2.ToString()); + ImGuiUtil.DrawTableColumn(t.Item3.ToString()); + }, ImGui.GetTextLineHeightWithSpacing()); + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private void DrawCollectionPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Collections"); + if (!p) + return; + + using (var t = ImRaii.Table("collections", 2, ImGuiTableFlags.SizingFixedFit)) + { + if (t) + foreach (var collection in _collections) + { + ImGui.TableNextColumn(); + DrawCollection((collection.Key, collection.Value)); + } + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private static void DrawCollection((Guid Id, string Name)? collection) + { + if (collection == null) + { + ImGui.TextUnformatted(""); + ImGui.TableNextColumn(); + return; + } + + ImGui.TextUnformatted(collection.Value.Name); + ImGui.TableNextColumn(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.CopyOnClickSelectable(collection.Value.Id.ToString()); + } + } +} diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs new file mode 100644 index 00000000..94b1e4e8 --- /dev/null +++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs @@ -0,0 +1,70 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class EditingIpcTester(DalamudPluginInterface pi) : IUiService +{ + private string _inputPath = string.Empty; + private string _inputPath2 = string.Empty; + private string _outputPath = string.Empty; + private string _outputPath2 = string.Empty; + + private TextureType _typeSelector; + private bool _mipMaps = true; + + private Task? _task1; + private Task? _task2; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Editing"); + if (!_) + return; + + ImGui.InputTextWithHint("##inputPath", "Input Texture Path...", ref _inputPath, 256); + ImGui.InputTextWithHint("##outputPath", "Output Texture Path...", ref _outputPath, 256); + ImGui.InputTextWithHint("##inputPath2", "Input Texture Path 2...", ref _inputPath2, 256); + ImGui.InputTextWithHint("##outputPath2", "Output Texture Path 2...", ref _outputPath2, 256); + TypeCombo(); + ImGui.Checkbox("Add MipMaps", ref _mipMaps); + + using var table = ImRaii.Table("...", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 1"); + if (ImGuiUtil.DrawDisabledButton("Save 1", Vector2.Zero, string.Empty, _task1 is { IsCompleted: false })) + _task1 = new ConvertTextureFile(pi).Invoke(_inputPath, _outputPath, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task1 == null ? "Not Initiated" : _task1.Status.ToString()); + if (ImGui.IsItemHovered() && _task1?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task1.Exception?.ToString()); + + IpcTester.DrawIntro(ConvertTextureFile.Label, (string)"Convert Texture 2"); + if (ImGuiUtil.DrawDisabledButton("Save 2", Vector2.Zero, string.Empty, _task2 is { IsCompleted: false })) + _task2 = new ConvertTextureFile(pi).Invoke(_inputPath2, _outputPath2, _typeSelector, _mipMaps); + ImGui.SameLine(); + ImGui.TextUnformatted(_task2 == null ? "Not Initiated" : _task2.Status.ToString()); + if (ImGui.IsItemHovered() && _task2?.Status == TaskStatus.Faulted) + ImGui.SetTooltip(_task2.Exception?.ToString()); + } + + private void TypeCombo() + { + using var combo = ImRaii.Combo("Convert To", _typeSelector.ToString()); + if (!combo) + return; + + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), _typeSelector == value)) + _typeSelector = value; + } + } +} diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs new file mode 100644 index 00000000..2c41b882 --- /dev/null +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -0,0 +1,137 @@ +using Dalamud.Interface; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.String; + +namespace Penumbra.Api.IpcTester; + +public class GameStateIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber CharacterBaseCreating; + public readonly EventSubscriber CharacterBaseCreated; + public readonly EventSubscriber GameObjectResourcePathResolved; + + private string _lastCreatedGameObjectName = string.Empty; + private nint _lastCreatedDrawObject = nint.Zero; + private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue; + private string _lastResolvedGamePath = string.Empty; + private string _lastResolvedFullPath = string.Empty; + private string _lastResolvedObject = string.Empty; + private DateTimeOffset _lastResolvedGamePathTime = DateTimeOffset.MaxValue; + private string _currentDrawObjectString = string.Empty; + private nint _currentDrawObject = nint.Zero; + private int _currentCutsceneActor; + private int _currentCutsceneParent; + private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; + + public GameStateIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + CharacterBaseCreating = CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); + CharacterBaseCreated = CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); + GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); + } + + public void Dispose() + { + CharacterBaseCreating.Dispose(); + CharacterBaseCreated.Dispose(); + GameObjectResourcePathResolved.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Game State"); + if (!_) + return; + + if (ImGui.InputTextWithHint("##drawObject", "Draw Object Address..", ref _currentDrawObjectString, 16, + ImGuiInputTextFlags.CharsHexadecimal)) + _currentDrawObject = nint.TryParse(_currentDrawObjectString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, + out var tmp) + ? tmp + : nint.Zero; + + ImGui.InputInt("Cutscene Actor", ref _currentCutsceneActor, 0); + ImGui.InputInt("Cutscene Parent", ref _currentCutsceneParent, 0); + if (_cutsceneError is not PenumbraApiEc.Success) + { + ImGui.SameLine(); + ImGui.TextUnformatted("Invalid Argument on last Call"); + } + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetDrawObjectInfo.Label, "Draw Object Info"); + if (_currentDrawObject == nint.Zero) + { + ImGui.TextUnformatted("Invalid"); + } + else + { + var (ptr, (collectionId, collectionName)) = new GetDrawObjectInfo(_pi).Invoke(_currentDrawObject); + ImGui.TextUnformatted(ptr == nint.Zero ? $"No Actor Associated, {collectionName}" : $"{ptr:X}, {collectionName}"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGui.TextUnformatted(collectionId.ToString()); + } + } + + IpcTester.DrawIntro(GetCutsceneParentIndex.Label, "Cutscene Parent"); + ImGui.TextUnformatted(new GetCutsceneParentIndex(_pi).Invoke(_currentCutsceneActor).ToString()); + + IpcTester.DrawIntro(SetCutsceneParentIndex.Label, "Cutscene Parent"); + if (ImGui.Button("Set Parent")) + _cutsceneError = new SetCutsceneParentIndex(_pi) + .Invoke(_currentCutsceneActor, _currentCutsceneParent); + + IpcTester.DrawIntro(CreatingCharacterBase.Label, "Last Drawobject created"); + if (_lastCreatedGameObjectTime < DateTimeOffset.Now) + ImGui.TextUnformatted(_lastCreatedDrawObject != nint.Zero + ? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" + : $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"); + + IpcTester.DrawIntro(IpcSubscribers.GameObjectResourcePathResolved.Label, "Last GamePath resolved"); + if (_lastResolvedGamePathTime < DateTimeOffset.Now) + ImGui.TextUnformatted( + $"{_lastResolvedGamePath} -> {_lastResolvedFullPath} for <{_lastResolvedObject}> at {_lastResolvedGamePathTime}"); + } + + private void UpdateLastCreated(nint gameObject, Guid _, nint _2, nint _3, nint _4) + { + _lastCreatedGameObjectName = GetObjectName(gameObject); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = nint.Zero; + } + + private void UpdateLastCreated2(nint gameObject, Guid _, nint drawObject) + { + _lastCreatedGameObjectName = GetObjectName(gameObject); + _lastCreatedGameObjectTime = DateTimeOffset.Now; + _lastCreatedDrawObject = drawObject; + } + + private void UpdateGameObjectResourcePath(nint gameObject, string gamePath, string fullPath) + { + _lastResolvedObject = GetObjectName(gameObject); + _lastResolvedGamePath = gamePath; + _lastResolvedFullPath = fullPath; + _lastResolvedGamePathTime = DateTimeOffset.Now; + } + + private static unsafe string GetObjectName(nint gameObject) + { + var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; + var name = obj != null ? obj->Name : null; + return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; + } +} diff --git a/Penumbra/Api/IpcTester/IpcTester.cs b/Penumbra/Api/IpcTester/IpcTester.cs new file mode 100644 index 00000000..201e7068 --- /dev/null +++ b/Penumbra/Api/IpcTester/IpcTester.cs @@ -0,0 +1,133 @@ +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.System.Framework; +using ImGuiNET; +using OtterGui.Services; +using Penumbra.Api.Api; + +namespace Penumbra.Api.IpcTester; + +public class IpcTester( + IpcProviders ipcProviders, + IPenumbraApi api, + PluginStateIpcTester pluginStateIpcTester, + UiIpcTester uiIpcTester, + RedrawingIpcTester redrawingIpcTester, + GameStateIpcTester gameStateIpcTester, + ResolveIpcTester resolveIpcTester, + CollectionsIpcTester collectionsIpcTester, + MetaIpcTester metaIpcTester, + ModsIpcTester modsIpcTester, + ModSettingsIpcTester modSettingsIpcTester, + EditingIpcTester editingIpcTester, + TemporaryIpcTester temporaryIpcTester, + ResourceTreeIpcTester resourceTreeIpcTester, + IFramework framework) : IUiService +{ + private readonly IpcProviders _ipcProviders = ipcProviders; + private DateTime _lastUpdate; + private bool _subscribed = false; + + public void Draw() + { + try + { + _lastUpdate = framework.LastUpdateUTC.AddSeconds(1); + Subscribe(); + + ImGui.TextUnformatted($"API Version: {api.ApiVersion.Breaking}.{api.ApiVersion.Feature:D4}"); + collectionsIpcTester.Draw(); + editingIpcTester.Draw(); + gameStateIpcTester.Draw(); + metaIpcTester.Draw(); + modSettingsIpcTester.Draw(); + modsIpcTester.Draw(); + pluginStateIpcTester.Draw(); + redrawingIpcTester.Draw(); + resolveIpcTester.Draw(); + resourceTreeIpcTester.Draw(); + uiIpcTester.Draw(); + temporaryIpcTester.Draw(); + temporaryIpcTester.DrawCollections(); + temporaryIpcTester.DrawMods(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error during IPC Tests:\n{e}"); + } + } + + internal static void DrawIntro(string label, string info) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(label); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(info); + ImGui.TableNextColumn(); + } + + private void Subscribe() + { + if (_subscribed) + return; + + Penumbra.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester."); + gameStateIpcTester.GameObjectResourcePathResolved.Enable(); + gameStateIpcTester.CharacterBaseCreated.Enable(); + gameStateIpcTester.CharacterBaseCreating.Enable(); + modSettingsIpcTester.SettingChanged.Enable(); + modsIpcTester.DeleteSubscriber.Enable(); + modsIpcTester.AddSubscriber.Enable(); + modsIpcTester.MoveSubscriber.Enable(); + pluginStateIpcTester.ModDirectoryChanged.Enable(); + pluginStateIpcTester.Initialized.Enable(); + pluginStateIpcTester.Disposed.Enable(); + pluginStateIpcTester.EnabledChange.Enable(); + redrawingIpcTester.Redrawn.Enable(); + uiIpcTester.PreSettingsTabBar.Enable(); + uiIpcTester.PreSettingsPanel.Enable(); + uiIpcTester.PostEnabled.Enable(); + uiIpcTester.PostSettingsPanelDraw.Enable(); + uiIpcTester.ChangedItemTooltip.Enable(); + uiIpcTester.ChangedItemClicked.Enable(); + + framework.Update += CheckUnsubscribe; + _subscribed = true; + } + + private void CheckUnsubscribe(IFramework framework1) + { + if (_lastUpdate > framework.LastUpdateUTC) + return; + + Unsubscribe(); + framework.Update -= CheckUnsubscribe; + } + + private void Unsubscribe() + { + if (!_subscribed) + return; + + Penumbra.Log.Debug("[IPCTester] Unsubscribed from IPC events for IPC tester."); + _subscribed = false; + gameStateIpcTester.GameObjectResourcePathResolved.Disable(); + gameStateIpcTester.CharacterBaseCreated.Disable(); + gameStateIpcTester.CharacterBaseCreating.Disable(); + modSettingsIpcTester.SettingChanged.Disable(); + modsIpcTester.DeleteSubscriber.Disable(); + modsIpcTester.AddSubscriber.Disable(); + modsIpcTester.MoveSubscriber.Disable(); + pluginStateIpcTester.ModDirectoryChanged.Disable(); + pluginStateIpcTester.Initialized.Disable(); + pluginStateIpcTester.Disposed.Disable(); + pluginStateIpcTester.EnabledChange.Disable(); + redrawingIpcTester.Redrawn.Disable(); + uiIpcTester.PreSettingsTabBar.Disable(); + uiIpcTester.PreSettingsPanel.Disable(); + uiIpcTester.PostEnabled.Disable(); + uiIpcTester.PostSettingsPanelDraw.Disable(); + uiIpcTester.ChangedItemTooltip.Disable(); + uiIpcTester.ChangedItemClicked.Disable(); + } +} diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs new file mode 100644 index 00000000..3fa7de7f --- /dev/null +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class MetaIpcTester(DalamudPluginInterface pi) : IUiService +{ + private int _gameObjectIndex; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Meta"); + if (!_) + return; + + ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetPlayerMetaManipulations.Label, "Player Meta Manipulations"); + if (ImGui.Button("Copy to Clipboard##Player")) + { + var base64 = new GetPlayerMetaManipulations(pi).Invoke(); + ImGui.SetClipboardText(base64); + } + + IpcTester.DrawIntro(GetMetaManipulations.Label, "Game Object Manipulations"); + if (ImGui.Button("Copy to Clipboard##GameObject")) + { + var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex); + ImGui.SetClipboardText(base64); + } + } +} diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs new file mode 100644 index 00000000..c33fcdee --- /dev/null +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -0,0 +1,181 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.UI; + +namespace Penumbra.Api.IpcTester; + +public class ModSettingsIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber SettingChanged; + + private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; + private ModSettingChange _lastSettingChangeType; + private Guid _lastSettingChangeCollection = Guid.Empty; + private string _lastSettingChangeMod = string.Empty; + private bool _lastSettingChangeInherited; + private DateTimeOffset _lastSettingChange; + + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private Guid? _settingsCollection; + private string _settingsCollectionName = string.Empty; + private bool _settingsIgnoreInheritance; + private bool _settingsInherit; + private bool _settingsEnabled; + private int _settingsPriority; + private IReadOnlyDictionary? _availableSettings; + private Dictionary>? _currentSettings; + + public ModSettingsIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting); + } + + public void Dispose() + { + SettingChanged.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Mod Settings"); + if (!_) + return; + + ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); + ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); + ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName); + ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance); + var collection = _settingsCollection.GetValueOrDefault(Guid.Empty); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro("Last Error", _lastSettingsError.ToString()); + + IpcTester.DrawIntro(ModSettingChanged.Label, "Last Mod Setting Changed"); + ImGui.TextUnformatted(_lastSettingChangeMod.Length > 0 + ? $"{_lastSettingChangeType} of {_lastSettingChangeMod} in {_lastSettingChangeCollection}{(_lastSettingChangeInherited ? " (Inherited)" : string.Empty)} at {_lastSettingChange}" + : "None"); + + IpcTester.DrawIntro(GetAvailableModSettings.Label, "Get Available Settings"); + if (ImGui.Button("Get##Available")) + { + _availableSettings = new GetAvailableModSettings(_pi).Invoke(_settingsModDirectory, _settingsModName); + _lastSettingsError = _availableSettings == null ? PenumbraApiEc.ModMissing : PenumbraApiEc.Success; + } + + IpcTester.DrawIntro(GetCurrentModSettings.Label, "Get Current Settings"); + if (ImGui.Button("Get##Current")) + { + var ret = new GetCurrentModSettings(_pi) + .Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance); + _lastSettingsError = ret.Item1; + if (ret.Item1 == PenumbraApiEc.Success) + { + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? true; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; + } + else + { + _currentSettings = null; + } + } + + IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod"); + ImGui.Checkbox("##inherit", ref _settingsInherit); + ImGui.SameLine(); + if (ImGui.Button("Set##Inherit")) + _lastSettingsError = new TryInheritMod(_pi) + .Invoke(collection, _settingsModDirectory, _settingsInherit, _settingsModName); + + IpcTester.DrawIntro(TrySetMod.Label, "Set Enabled"); + ImGui.Checkbox("##enabled", ref _settingsEnabled); + ImGui.SameLine(); + if (ImGui.Button("Set##Enabled")) + _lastSettingsError = new TrySetMod(_pi) + .Invoke(collection, _settingsModDirectory, _settingsEnabled, _settingsModName); + + IpcTester.DrawIntro(TrySetModPriority.Label, "Set Priority"); + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + ImGui.DragInt("##Priority", ref _settingsPriority); + ImGui.SameLine(); + if (ImGui.Button("Set##Priority")) + _lastSettingsError = new TrySetModPriority(_pi) + .Invoke(collection, _settingsModDirectory, _settingsPriority, _settingsModName); + + IpcTester.DrawIntro(CopyModSettings.Label, "Copy Mod Settings"); + if (ImGui.Button("Copy Settings")) + _lastSettingsError = new CopyModSettings(_pi) + .Invoke(_settingsCollection, _settingsModDirectory, _settingsModName); + + ImGuiUtil.HoverTooltip("Copy settings from Mod Directory Name to Mod Name (as directory) in collection."); + + IpcTester.DrawIntro(TrySetModSetting.Label, "Set Setting(s)"); + if (_availableSettings == null) + return; + + foreach (var (group, (list, type)) in _availableSettings) + { + using var id = ImRaii.PushId(group); + var preview = list.Length > 0 ? list[0] : string.Empty; + if (_currentSettings != null && _currentSettings.TryGetValue(group, out var current) && current.Count > 0) + { + preview = current[0]; + } + else + { + current = []; + if (_currentSettings != null) + _currentSettings[group] = current; + } + + ImGui.SetNextItemWidth(200 * UiHelpers.Scale); + using (var c = ImRaii.Combo("##group", preview)) + { + if (c) + foreach (var s in list) + { + var contained = current.Contains(s); + if (ImGui.Checkbox(s, ref contained)) + { + if (contained) + current.Add(s); + else + current.Remove(s); + } + } + } + + ImGui.SameLine(); + if (ImGui.Button("Set##setting")) + _lastSettingsError = type == GroupType.Single + ? new TrySetModSetting(_pi).Invoke(collection, _settingsModDirectory, group, current.Count > 0 ? current[0] : string.Empty, + _settingsModName) + : new TrySetModSettings(_pi).Invoke(collection, _settingsModDirectory, group, current.ToArray(), _settingsModName); + + ImGui.SameLine(); + ImGui.TextUnformatted(group); + } + } + + private void UpdateLastModSetting(ModSettingChange type, Guid collection, string mod, bool inherited) + { + _lastSettingChangeType = type; + _lastSettingChangeCollection = collection; + _lastSettingChangeMod = mod; + _lastSettingChangeInherited = inherited; + _lastSettingChange = DateTimeOffset.Now; + } +} diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs new file mode 100644 index 00000000..878a8214 --- /dev/null +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -0,0 +1,154 @@ +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class ModsIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private string _newInstallPath = string.Empty; + private PenumbraApiEc _lastReloadEc; + private PenumbraApiEc _lastAddEc; + private PenumbraApiEc _lastDeleteEc; + private PenumbraApiEc _lastSetPathEc; + private PenumbraApiEc _lastInstallEc; + private Dictionary _mods = []; + + public readonly EventSubscriber DeleteSubscriber; + public readonly EventSubscriber AddSubscriber; + public readonly EventSubscriber MoveSubscriber; + + private DateTimeOffset _lastDeletedModTime = DateTimeOffset.UnixEpoch; + private string _lastDeletedMod = string.Empty; + private DateTimeOffset _lastAddedModTime = DateTimeOffset.UnixEpoch; + private string _lastAddedMod = string.Empty; + private DateTimeOffset _lastMovedModTime = DateTimeOffset.UnixEpoch; + private string _lastMovedModFrom = string.Empty; + private string _lastMovedModTo = string.Empty; + + public ModsIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + DeleteSubscriber = ModDeleted.Subscriber(pi, s => + { + _lastDeletedModTime = DateTimeOffset.UtcNow; + _lastDeletedMod = s; + }); + AddSubscriber = ModAdded.Subscriber(pi, s => + { + _lastAddedModTime = DateTimeOffset.UtcNow; + _lastAddedMod = s; + }); + MoveSubscriber = ModMoved.Subscriber(pi, (s1, s2) => + { + _lastMovedModTime = DateTimeOffset.UtcNow; + _lastMovedModFrom = s1; + _lastMovedModTo = s2; + }); + } + + public void Dispose() + { + DeleteSubscriber.Dispose(); + AddSubscriber.Dispose(); + MoveSubscriber.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Mods"); + if (!_) + return; + + ImGui.InputTextWithHint("##install", "Install File Path...", ref _newInstallPath, 100); + ImGui.InputTextWithHint("##modDir", "Mod Directory Name...", ref _modDirectory, 100); + ImGui.InputTextWithHint("##modName", "Mod Name...", ref _modName, 100); + ImGui.InputTextWithHint("##path", "New Path...", ref _pathInput, 100); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetModList.Label, "Mods"); + DrawModsPopup(); + if (ImGui.Button("Get##Mods")) + { + _mods = new GetModList(_pi).Invoke(); + ImGui.OpenPopup("Mods"); + } + + IpcTester.DrawIntro(ReloadMod.Label, "Reload Mod"); + if (ImGui.Button("Reload")) + _lastReloadEc = new ReloadMod(_pi).Invoke(_modDirectory, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastReloadEc.ToString()); + + IpcTester.DrawIntro(InstallMod.Label, "Install Mod"); + if (ImGui.Button("Install")) + _lastInstallEc = new InstallMod(_pi).Invoke(_newInstallPath); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastInstallEc.ToString()); + + IpcTester.DrawIntro(AddMod.Label, "Add Mod"); + if (ImGui.Button("Add")) + _lastAddEc = new AddMod(_pi).Invoke(_modDirectory); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastAddEc.ToString()); + + IpcTester.DrawIntro(DeleteMod.Label, "Delete Mod"); + if (ImGui.Button("Delete")) + _lastDeleteEc = new DeleteMod(_pi).Invoke(_modDirectory, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastDeleteEc.ToString()); + + IpcTester.DrawIntro(GetModPath.Label, "Current Path"); + var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName); + ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]"); + + IpcTester.DrawIntro(SetModPath.Label, "Set Path"); + if (ImGui.Button("Set")) + _lastSetPathEc = new SetModPath(_pi).Invoke(_modDirectory, _pathInput, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastSetPathEc.ToString()); + + IpcTester.DrawIntro(ModDeleted.Label, "Last Mod Deleted"); + if (_lastDeletedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastDeletedMod} at {_lastDeletedModTime}"); + + IpcTester.DrawIntro(ModAdded.Label, "Last Mod Added"); + if (_lastAddedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastAddedMod} at {_lastAddedModTime}"); + + IpcTester.DrawIntro(ModMoved.Label, "Last Mod Moved"); + if (_lastMovedModTime > DateTimeOffset.UnixEpoch) + ImGui.TextUnformatted($"{_lastMovedModFrom} -> {_lastMovedModTo} at {_lastMovedModTime}"); + } + + private void DrawModsPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImRaii.Popup("Mods"); + if (!p) + return; + + foreach (var (modDir, modName) in _mods) + ImGui.TextUnformatted($"{modDir}: {modName}"); + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } +} diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs new file mode 100644 index 00000000..0588e5bd --- /dev/null +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -0,0 +1,132 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace Penumbra.Api.IpcTester; + +public class PluginStateIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber ModDirectoryChanged; + public readonly EventSubscriber Initialized; + public readonly EventSubscriber Disposed; + public readonly EventSubscriber EnabledChange; + + private string _currentConfiguration = string.Empty; + private string _lastModDirectory = string.Empty; + private bool _lastModDirectoryValid; + private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue; + + private readonly List _initializedList = []; + private readonly List _disposedList = []; + + private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; + private bool? _lastEnabledValue; + + public PluginStateIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); + Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized); + Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed); + EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled); + } + + public void Dispose() + { + ModDirectoryChanged.Dispose(); + Initialized.Dispose(); + Disposed.Dispose(); + EnabledChange.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Plugin State"); + if (!_) + return; + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + DrawList(IpcSubscribers.Initialized.Label, "Last Initialized", _initializedList); + DrawList(IpcSubscribers.Disposed.Label, "Last Disposed", _disposedList); + + IpcTester.DrawIntro(ApiVersion.Label, "Current Version"); + var (breaking, features) = new ApiVersion(_pi).Invoke(); + ImGui.TextUnformatted($"{breaking}.{features:D4}"); + + IpcTester.DrawIntro(GetEnabledState.Label, "Current State"); + ImGui.TextUnformatted($"{new GetEnabledState(_pi).Invoke()}"); + + IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change"); + ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); + + DrawConfigPopup(); + IpcTester.DrawIntro(GetConfiguration.Label, "Configuration"); + if (ImGui.Button("Get")) + { + _currentConfiguration = new GetConfiguration(_pi).Invoke(); + ImGui.OpenPopup("Config Popup"); + } + + IpcTester.DrawIntro(GetModDirectory.Label, "Current Mod Directory"); + ImGui.TextUnformatted(new GetModDirectory(_pi).Invoke()); + + IpcTester.DrawIntro(IpcSubscribers.ModDirectoryChanged.Label, "Last Mod Directory Change"); + ImGui.TextUnformatted(_lastModDirectoryTime > DateTimeOffset.MinValue + ? $"{_lastModDirectory} ({(_lastModDirectoryValid ? "Valid" : "Invalid")}) at {_lastModDirectoryTime}" + : "None"); + + void DrawList(string label, string text, List list) + { + IpcTester.DrawIntro(label, text); + if (list.Count == 0) + { + ImGui.TextUnformatted("Never"); + } + else + { + ImGui.TextUnformatted(list[^1].LocalDateTime.ToString(CultureInfo.CurrentCulture)); + if (list.Count > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", + list.SkipLast(1).Select(t => t.LocalDateTime.ToString(CultureInfo.CurrentCulture)))); + } + } + } + + private void DrawConfigPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var popup = ImRaii.Popup("Config Popup"); + if (!popup) + return; + + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.TextWrapped(_currentConfiguration); + } + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } + + private void UpdateModDirectoryChanged(string path, bool valid) + => (_lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime) = (path, valid, DateTimeOffset.Now); + + private void AddInitialized() + => _initializedList.Add(DateTimeOffset.UtcNow); + + private void AddDisposed() + => _disposedList.Add(DateTimeOffset.UtcNow); + + private void SetLastEnabled(bool val) + => (_lastEnabledChange, _lastEnabledValue) = (DateTimeOffset.Now, val); +} diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs new file mode 100644 index 00000000..281c7ad4 --- /dev/null +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -0,0 +1,72 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.GameData.Interop; +using Penumbra.UI; + +namespace Penumbra.Api.IpcTester; + +public class RedrawingIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + private readonly ObjectManager _objects; + public readonly EventSubscriber Redrawn; + + private int _redrawIndex; + private string _lastRedrawnString = "None"; + + public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects) + { + _pi = pi; + _objects = objects; + Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); + } + + public void Dispose() + { + Redrawn.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("Redrawing"); + if (!_) + return; + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(RedrawObject.Label, "Redraw by Index"); + var tmp = _redrawIndex; + ImGui.SetNextItemWidth(100 * UiHelpers.Scale); + if (ImGui.DragInt("##redrawIndex", ref tmp, 0.1f, 0, _objects.TotalCount)) + _redrawIndex = Math.Clamp(tmp, 0, _objects.TotalCount); + ImGui.SameLine(); + if (ImGui.Button("Redraw##Index")) + new RedrawObject(_pi).Invoke(_redrawIndex); + + IpcTester.DrawIntro(RedrawAll.Label, "Redraw All"); + if (ImGui.Button("Redraw##All")) + new RedrawAll(_pi).Invoke(); + + IpcTester.DrawIntro(GameObjectRedrawn.Label, "Last Redrawn Object:"); + ImGui.TextUnformatted(_lastRedrawnString); + } + + private void SetLastRedrawn(nint address, int index) + { + if (index < 0 + || index > _objects.TotalCount + || address == nint.Zero + || _objects[index].Address != address) + _lastRedrawnString = "Invalid"; + + _lastRedrawnString = $"{_objects[index].Utf8Name} (0x{address:X}, {index})"; + } +} diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs new file mode 100644 index 00000000..978ed8d6 --- /dev/null +++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs @@ -0,0 +1,114 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.IpcSubscribers; +using Penumbra.String.Classes; + +namespace Penumbra.Api.IpcTester; + +public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService +{ + private string _currentResolvePath = string.Empty; + private string _currentReversePath = string.Empty; + private int _currentReverseIdx; + private Task<(string[], string[][])> _task = Task.FromResult<(string[], string[][])>(([], [])); + + public void Draw() + { + using var tree = ImRaii.TreeNode("Resolving"); + if (!tree) + return; + + ImGui.InputTextWithHint("##resolvePath", "Resolve this game path...", ref _currentResolvePath, Utf8GamePath.MaxGamePathLength); + ImGui.InputTextWithHint("##resolveInversePath", "Reverse-resolve this path...", ref _currentReversePath, + Utf8GamePath.MaxGamePathLength); + ImGui.InputInt("##resolveIdx", ref _currentReverseIdx, 0, 0); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(ResolveDefaultPath.Label, "Default Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolveDefaultPath(pi).Invoke(_currentResolvePath)); + + IpcTester.DrawIntro(ResolveInterfacePath.Label, "Interface Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolveInterfacePath(pi).Invoke(_currentResolvePath)); + + IpcTester.DrawIntro(ResolvePlayerPath.Label, "Player Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolvePlayerPath(pi).Invoke(_currentResolvePath)); + + IpcTester.DrawIntro(ResolveGameObjectPath.Label, "Game Object Collection Resolve"); + if (_currentResolvePath.Length != 0) + ImGui.TextUnformatted(new ResolveGameObjectPath(pi).Invoke(_currentResolvePath, _currentReverseIdx)); + + IpcTester.DrawIntro(ReverseResolvePlayerPath.Label, "Reversed Game Paths (Player)"); + if (_currentReversePath.Length > 0) + { + var list = new ReverseResolvePlayerPath(pi).Invoke(_currentReversePath); + if (list.Length > 0) + { + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); + } + } + + IpcTester.DrawIntro(ReverseResolveGameObjectPath.Label, "Reversed Game Paths (Game Object)"); + if (_currentReversePath.Length > 0) + { + var list = new ReverseResolveGameObjectPath(pi).Invoke(_currentReversePath, _currentReverseIdx); + if (list.Length > 0) + { + ImGui.TextUnformatted(list[0]); + if (list.Length > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", list.Skip(1))); + } + } + + var forwardArray = _currentResolvePath.Length > 0 + ? [_currentResolvePath] + : Array.Empty(); + var reverseArray = _currentReversePath.Length > 0 + ? [_currentReversePath] + : Array.Empty(); + + IpcTester.DrawIntro(ResolvePlayerPaths.Label, "Resolved Paths (Player)"); + if (forwardArray.Length > 0 || reverseArray.Length > 0) + { + var ret = new ResolvePlayerPaths(pi).Invoke(forwardArray, reverseArray); + ImGui.TextUnformatted(ConvertText(ret)); + } + + IpcTester.DrawIntro(ResolvePlayerPathsAsync.Label, "Resolved Paths Async (Player)"); + if (ImGui.Button("Start")) + _task = new ResolvePlayerPathsAsync(pi).Invoke(forwardArray, reverseArray); + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(_task.Status.ToString()); + if ((hovered || ImGui.IsItemHovered()) && _task.IsCompletedSuccessfully) + ImGui.SetTooltip(ConvertText(_task.Result)); + return; + + static string ConvertText((string[], string[][]) data) + { + var text = string.Empty; + if (data.Item1.Length > 0) + { + if (data.Item2.Length > 0) + text = $"Forward: {data.Item1[0]} | Reverse: {string.Join("; ", data.Item2[0])}."; + else + text = $"Forward: {data.Item1[0]}."; + } + else if (data.Item2.Length > 0) + { + text = $"Reverse: {string.Join("; ", data.Item2[0])}."; + } + + return text; + } + } +} diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs new file mode 100644 index 00000000..1f57fc9d --- /dev/null +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -0,0 +1,349 @@ +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Penumbra.Api.IpcTester; + +public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService +{ + private readonly Stopwatch _stopwatch = new(); + + private string _gameObjectIndices = "0"; + private ResourceType _type = ResourceType.Mtrl; + private bool _withUiData; + + private (string, Dictionary>?)[]? _lastGameObjectResourcePaths; + private (string, Dictionary>?)[]? _lastPlayerResourcePaths; + private (string, IReadOnlyDictionary?)[]? _lastGameObjectResourcesOfType; + private (string, IReadOnlyDictionary?)[]? _lastPlayerResourcesOfType; + private (string, ResourceTreeDto?)[]? _lastGameObjectResourceTrees; + private (string, ResourceTreeDto)[]? _lastPlayerResourceTrees; + private TimeSpan _lastCallDuration; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Resource Tree"); + if (!_) + return; + + ImGui.InputText("GameObject indices", ref _gameObjectIndices, 511); + ImGuiUtil.GenericEnumCombo("Resource type", ImGui.CalcItemWidth(), _type, out _type, Enum.GetValues()); + ImGui.Checkbox("Also get names and icons", ref _withUiData); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(GetGameObjectResourcePaths.Label, "Get GameObject resource paths"); + if (ImGui.Button("Get##GameObjectResourcePaths")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = new GetGameObjectResourcePaths(pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourcePaths = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(resourcePaths) + .ToArray(); + + ImGui.OpenPopup(nameof(GetGameObjectResourcePaths)); + } + + IpcTester.DrawIntro(GetPlayerResourcePaths.Label, "Get local player resource paths"); + if (ImGui.Button("Get##PlayerResourcePaths")) + { + var subscriber = new GetPlayerResourcePaths(pi); + _stopwatch.Restart(); + var resourcePaths = subscriber.Invoke(); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcePaths = resourcePaths + .Select(pair => (GameObjectToString(pair.Key), pair.Value)) + .ToArray()!; + + ImGui.OpenPopup(nameof(GetPlayerResourcePaths)); + } + + IpcTester.DrawIntro(GetGameObjectResourcesOfType.Label, "Get GameObject resources of type"); + if (ImGui.Button("Get##GameObjectResourcesOfType")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = new GetGameObjectResourcesOfType(pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUiData, gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourcesOfType = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(resourcesOfType) + .ToArray(); + + ImGui.OpenPopup(nameof(GetGameObjectResourcesOfType)); + } + + IpcTester.DrawIntro(GetPlayerResourcesOfType.Label, "Get local player resources of type"); + if (ImGui.Button("Get##PlayerResourcesOfType")) + { + var subscriber = new GetPlayerResourcesOfType(pi); + _stopwatch.Restart(); + var resourcesOfType = subscriber.Invoke(_type, _withUiData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourcesOfType = resourcesOfType + .Select(pair => (GameObjectToString(pair.Key), (IReadOnlyDictionary?)pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(GetPlayerResourcesOfType)); + } + + IpcTester.DrawIntro(GetGameObjectResourceTrees.Label, "Get GameObject resource trees"); + if (ImGui.Button("Get##GameObjectResourceTrees")) + { + var gameObjects = GetSelectedGameObjects(); + var subscriber = new GetGameObjectResourceTrees(pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUiData, gameObjects); + + _lastCallDuration = _stopwatch.Elapsed; + _lastGameObjectResourceTrees = gameObjects + .Select(i => GameObjectToString(i)) + .Zip(trees) + .ToArray(); + + ImGui.OpenPopup(nameof(GetGameObjectResourceTrees)); + } + + IpcTester.DrawIntro(GetPlayerResourceTrees.Label, "Get local player resource trees"); + if (ImGui.Button("Get##PlayerResourceTrees")) + { + var subscriber = new GetPlayerResourceTrees(pi); + _stopwatch.Restart(); + var trees = subscriber.Invoke(_withUiData); + + _lastCallDuration = _stopwatch.Elapsed; + _lastPlayerResourceTrees = trees + .Select(pair => (GameObjectToString(pair.Key), pair.Value)) + .ToArray(); + + ImGui.OpenPopup(nameof(GetPlayerResourceTrees)); + } + + DrawPopup(nameof(GetGameObjectResourcePaths), ref _lastGameObjectResourcePaths, DrawResourcePaths, + _lastCallDuration); + DrawPopup(nameof(GetPlayerResourcePaths), ref _lastPlayerResourcePaths!, DrawResourcePaths, _lastCallDuration); + + DrawPopup(nameof(GetGameObjectResourcesOfType), ref _lastGameObjectResourcesOfType, DrawResourcesOfType, + _lastCallDuration); + DrawPopup(nameof(GetPlayerResourcesOfType), ref _lastPlayerResourcesOfType, DrawResourcesOfType, + _lastCallDuration); + + DrawPopup(nameof(GetGameObjectResourceTrees), ref _lastGameObjectResourceTrees, DrawResourceTrees, + _lastCallDuration); + DrawPopup(nameof(GetPlayerResourceTrees), ref _lastPlayerResourceTrees, DrawResourceTrees!, _lastCallDuration); + } + + private static void DrawPopup(string popupId, ref T? result, Action drawResult, TimeSpan duration) where T : class + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(1000, 500)); + using var popup = ImRaii.Popup(popupId); + if (!popup) + { + result = null; + return; + } + + if (result == null) + { + ImGui.CloseCurrentPopup(); + return; + } + + drawResult(result); + + ImGui.TextUnformatted($"Invoked in {duration.TotalMilliseconds} ms"); + + if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + { + result = null; + ImGui.CloseCurrentPopup(); + } + } + + private static void DrawWithHeaders((string, T?)[] result, Action drawItem) where T : class + { + var firstSeen = new Dictionary(); + foreach (var (label, item) in result) + { + if (item == null) + { + ImRaii.TreeNode($"{label}: null", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + if (firstSeen.TryGetValue(item, out var firstLabel)) + { + ImRaii.TreeNode($"{label}: same as {firstLabel}", ImGuiTreeNodeFlags.Leaf).Dispose(); + continue; + } + + firstSeen.Add(item, label); + + using var header = ImRaii.TreeNode(label); + if (!header) + continue; + + drawItem(item); + } + } + + private static void DrawResourcePaths((string, Dictionary>?)[] result) + { + DrawWithHeaders(result, paths => + { + using var table = ImRaii.Table(string.Empty, 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Game Paths", ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (actualPath, gamePaths) in paths) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + ImGui.TableNextColumn(); + foreach (var gamePath in gamePaths) + ImGui.TextUnformatted(gamePath); + } + }); + } + + private void DrawResourcesOfType((string, IReadOnlyDictionary?)[] result) + { + DrawWithHeaders(result, resources => + { + using var table = ImRaii.Table(string.Empty, _withUiData ? 3 : 2, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, _withUiData ? 0.55f : 0.85f); + if (_withUiData) + ImGui.TableSetupColumn("Icon & Name", ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var (resourceHandle, (actualPath, name, icon)) in resources) + { + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{resourceHandle:X}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(actualPath); + if (_withUiData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(icon.ToString()); + ImGui.SameLine(); + ImGui.TextUnformatted(name); + } + } + }); + } + + private void DrawResourceTrees((string, ResourceTreeDto?)[] result) + { + DrawWithHeaders(result, tree => + { + ImGui.TextUnformatted($"Name: {tree.Name}\nRaceCode: {(GenderRace)tree.RaceCode}"); + + using var table = ImRaii.Table(string.Empty, _withUiData ? 7 : 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable); + if (!table) + return; + + if (_withUiData) + { + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.1f); + ImGui.TableSetupColumn("Icon", ImGuiTableColumnFlags.WidthStretch, 0.15f); + } + else + { + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthStretch, 0.5f); + } + + ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); + ImGui.TableSetupColumn("Object Address", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("Resource Handle", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableHeadersRow(); + + void DrawNode(ResourceNodeDto node) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + var hasChildren = node.Children.Any(); + using var treeNode = ImRaii.TreeNode( + $"{(_withUiData ? node.Name ?? "Unknown" : node.Type)}##{node.ObjectAddress:X8}", + hasChildren + ? ImGuiTreeNodeFlags.SpanFullWidth + : ImGuiTreeNodeFlags.SpanFullWidth | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen); + if (_withUiData) + { + ImGui.TableNextColumn(); + TextUnformattedMono(node.Type.ToString()); + ImGui.TableNextColumn(); + TextUnformattedMono(node.Icon.ToString()); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.GamePath ?? "Unknown"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(node.ActualPath); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ObjectAddress:X8}"); + ImGui.TableNextColumn(); + TextUnformattedMono($"0x{node.ResourceHandle:X8}"); + + if (treeNode) + foreach (var child in node.Children) + DrawNode(child); + } + + foreach (var node in tree.Nodes) + DrawNode(node); + }); + } + + private static void TextUnformattedMono(string text) + { + using var _ = ImRaii.PushFont(UiBuilder.MonoFont); + ImGui.TextUnformatted(text); + } + + private ushort[] GetSelectedGameObjects() + => _gameObjectIndices.Split(',') + .SelectWhere(index => (ushort.TryParse(index.Trim(), out var i), i)) + .ToArray(); + + private unsafe string GameObjectToString(ObjectIndex gameObjectIndex) + { + var gameObject = objects[gameObjectIndex]; + + return gameObject.Valid + ? $"[{gameObjectIndex}] {gameObject.Utf8Name} ({(ObjectKind)gameObject.AsObject->ObjectKind})" + : $"[{gameObjectIndex}] null"; + } +} diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs new file mode 100644 index 00000000..a8405eb2 --- /dev/null +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -0,0 +1,203 @@ +using Dalamud.Interface; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; +using Penumbra.Collections.Manager; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; + +namespace Penumbra.Api.IpcTester; + +public class TemporaryIpcTester( + DalamudPluginInterface pi, + ModManager modManager, + CollectionManager collections, + TempModManager tempMods, + TempCollectionManager tempCollections, + SaveService saveService, + Configuration config) + : IUiService +{ + public Guid LastCreatedCollectionId = Guid.Empty; + + private Guid? _tempGuid; + private string _tempCollectionName = string.Empty; + private string _tempCollectionGuidName = string.Empty; + private string _tempModName = string.Empty; + private string _tempGamePath = "test/game/path.mtrl"; + private string _tempFilePath = "test/success.mtrl"; + private string _tempManipulation = string.Empty; + private PenumbraApiEc _lastTempError; + private int _tempActorIndex; + private bool _forceOverwrite; + + public void Draw() + { + using var _ = ImRaii.TreeNode("Temporary"); + if (!_) + return; + + 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); + ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); + ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); + ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); + ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro("Last Error", _lastTempError.ToString()); + ImGuiUtil.DrawTableColumn("Last Created Collection"); + ImGui.TableNextColumn(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGuiUtil.CopyOnClickSelectable(LastCreatedCollectionId.ToString()); + } + + IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection"); + if (ImGui.Button("Create##Collection")) + { + LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName); + if (_tempGuid == null) + { + _tempGuid = LastCreatedCollectionId; + _tempCollectionGuidName = LastCreatedCollectionId.ToString(); + } + } + + var guid = _tempGuid.GetValueOrDefault(Guid.Empty); + + IpcTester.DrawIntro(DeleteTemporaryCollection.Label, "Delete Temporary Collection"); + if (ImGui.Button("Delete##Collection")) + _lastTempError = new DeleteTemporaryCollection(pi).Invoke(guid); + ImGui.SameLine(); + if (ImGui.Button("Delete Last##Collection")) + _lastTempError = new DeleteTemporaryCollection(pi).Invoke(LastCreatedCollectionId); + + IpcTester.DrawIntro(AssignTemporaryCollection.Label, "Assign Temporary Collection"); + if (ImGui.Button("Assign##NamedCollection")) + _lastTempError = new AssignTemporaryCollection(pi).Invoke(guid, _tempActorIndex, _forceOverwrite); + + IpcTester.DrawIntro(AddTemporaryMod.Label, "Add Temporary Mod to specific Collection"); + if (ImGui.Button("Add##Mod")) + _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, + new Dictionary { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); + + IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Copy Existing Collection"); + if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, + "Copies the effective list from the collection named in Temporary Mod Name...", + !collections.Storage.ByName(_tempModName, out var copyCollection)) + && copyCollection is { HasCache: true }) + { + var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); + var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), + MetaManipulation.CurrentVersion); + _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); + } + + IpcTester.DrawIntro(AddTemporaryModAll.Label, "Add Temporary Mod to all Collections"); + if (ImGui.Button("Add##All")) + _lastTempError = new AddTemporaryModAll(pi).Invoke(_tempModName, + new Dictionary { { _tempGamePath, _tempFilePath } }, + _tempManipulation.Length > 0 ? _tempManipulation : string.Empty, int.MaxValue); + + IpcTester.DrawIntro(RemoveTemporaryMod.Label, "Remove Temporary Mod from specific Collection"); + if (ImGui.Button("Remove##Mod")) + _lastTempError = new RemoveTemporaryMod(pi).Invoke(_tempModName, guid, int.MaxValue); + + IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); + if (ImGui.Button("Remove##ModAll")) + _lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue); + } + + public void DrawCollections() + { + using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); + if (!collTree) + return; + + using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + foreach (var (collection, idx) in tempCollections.Values.WithIndex()) + { + using var id = ImRaii.PushId(idx); + ImGui.TableNextColumn(); + var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) + .FirstOrDefault() + ?? "Unknown"; + if (ImGui.Button("Save##Collection")) + TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character); + + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(collection.Identifier); + } + + ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); + ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); + ImGuiUtil.DrawTableColumn(string.Join(", ", + tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName))); + } + } + + public void DrawMods() + { + using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods"); + if (!modTree) + return; + + using var table = ImRaii.Table("##modTree", 5, ImGuiTableFlags.SizingFixedFit); + + void PrintList(string collectionName, IReadOnlyList list) + { + foreach (var mod in list) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Priority.ToString()); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collectionName); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.Default.Files.Count.ToString()); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + foreach (var (path, file) in mod.Default.Files) + ImGui.TextUnformatted($"{path} -> {file}"); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mod.TotalManipulations.ToString()); + if (ImGui.IsItemHovered()) + { + using var tt = ImRaii.Tooltip(); + foreach (var manip in mod.Default.Manipulations) + ImGui.TextUnformatted(manip.ToString()); + } + } + } + + if (table) + { + PrintList("All", tempMods.ModsForAllCollections); + foreach (var (collection, list) in tempMods.Mods) + PrintList(collection.Name, list); + } + } +} diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs new file mode 100644 index 00000000..29ddc22e --- /dev/null +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -0,0 +1,128 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; +using Penumbra.Communication; + +namespace Penumbra.Api.IpcTester; + +public class UiIpcTester : IUiService, IDisposable +{ + private readonly DalamudPluginInterface _pi; + public readonly EventSubscriber PreSettingsTabBar; + public readonly EventSubscriber PreSettingsPanel; + public readonly EventSubscriber PostEnabled; + public readonly EventSubscriber PostSettingsPanelDraw; + public readonly EventSubscriber ChangedItemTooltip; + public readonly EventSubscriber ChangedItemClicked; + + private string _lastDrawnMod = string.Empty; + private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue; + private bool _subscribedToTooltip; + private bool _subscribedToClick; + private string _lastClicked = string.Empty; + private string _lastHovered = string.Empty; + private TabType _selectTab = TabType.None; + private string _modName = string.Empty; + private PenumbraApiEc _ec = PenumbraApiEc.Success; + + public UiIpcTester(DalamudPluginInterface pi) + { + _pi = pi; + PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); + PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod); + PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); + ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); + } + + public void Dispose() + { + PreSettingsTabBar.Dispose(); + PreSettingsPanel.Dispose(); + PostEnabled.Dispose(); + PostSettingsPanelDraw.Dispose(); + ChangedItemTooltip.Dispose(); + ChangedItemClicked.Dispose(); + } + + public void Draw() + { + using var _ = ImRaii.TreeNode("UI"); + if (!_) + return; + + using (var combo = ImRaii.Combo("Tab to Open at", _selectTab.ToString())) + { + if (combo) + foreach (var val in Enum.GetValues()) + { + if (ImGui.Selectable(val.ToString(), _selectTab == val)) + _selectTab = val; + } + } + + ImGui.InputTextWithHint("##openMod", "Mod to Open at...", ref _modName, 256); + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + IpcTester.DrawIntro(IpcSubscribers.PostSettingsPanelDraw.Label, "Last Drawn Mod"); + ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); + + IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip"); + if (ImGui.Checkbox("##tooltip", ref _subscribedToTooltip)) + { + if (_subscribedToTooltip) + ChangedItemTooltip.Enable(); + else + ChangedItemTooltip.Disable(); + } + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastHovered); + + IpcTester.DrawIntro(IpcSubscribers.ChangedItemClicked.Label, "Subscribe Click"); + if (ImGui.Checkbox("##click", ref _subscribedToClick)) + { + if (_subscribedToClick) + ChangedItemClicked.Enable(); + else + ChangedItemClicked.Disable(); + } + + ImGui.SameLine(); + ImGui.TextUnformatted(_lastClicked); + IpcTester.DrawIntro(OpenMainWindow.Label, "Open Mod Window"); + if (ImGui.Button("Open##window")) + _ec = new OpenMainWindow(_pi).Invoke(_selectTab, _modName, _modName); + + ImGui.SameLine(); + ImGui.TextUnformatted(_ec.ToString()); + + IpcTester.DrawIntro(CloseMainWindow.Label, "Close Mod Window"); + if (ImGui.Button("Close##window")) + new CloseMainWindow(_pi).Invoke(); + } + + private void UpdateLastDrawnMod(string name) + => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); + + private void UpdateLastDrawnMod(string name, float _1, float _2) + => (_lastDrawnMod, _lastDrawnModTime) = (name, DateTimeOffset.Now); + + private void AddedTooltip(ChangedItemType type, uint id) + { + _lastHovered = $"{type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; + ImGui.TextUnformatted("IPC Test Successful"); + } + + private void AddedClick(MouseButton button, ChangedItemType type, uint id) + { + _lastClicked = $"{button}-click on {type} {id} at {DateTime.UtcNow.ToLocalTime().ToString(CultureInfo.CurrentCulture)}"; + } +} diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs deleted file mode 100644 index dc1e8472..00000000 --- a/Penumbra/Api/PenumbraApi.cs +++ /dev/null @@ -1,1374 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin.Services; -using Lumina.Data; -using Newtonsoft.Json; -using OtterGui; -using Penumbra.Collections; -using Penumbra.Interop.PathResolving; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Compression; -using OtterGui.Log; -using Penumbra.Api.Enums; -using Penumbra.GameData.Actors; -using Penumbra.Interop.ResourceLoading; -using Penumbra.Mods.Manager; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.Services; -using Penumbra.Collections.Manager; -using Penumbra.Communication; -using Penumbra.GameData.Interop; -using Penumbra.Import.Textures; -using Penumbra.Interop.Services; -using Penumbra.UI; -using TextureType = Penumbra.Api.Enums.TextureType; -using Penumbra.Interop.ResourceTree; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; - -namespace Penumbra.Api; - -public class PenumbraApi : IDisposable, IPenumbraApi -{ - public (int, int) ApiVersion - => (4, 24); - - public event Action? PreSettingsTabBarDraw - { - add => _communicator.PreSettingsTabBarDraw.Subscribe(value!, Communication.PreSettingsTabBarDraw.Priority.Default); - remove => _communicator.PreSettingsTabBarDraw.Unsubscribe(value!); - } - - public event Action? PreSettingsPanelDraw - { - add => _communicator.PreSettingsPanelDraw.Subscribe(value!, Communication.PreSettingsPanelDraw.Priority.Default); - remove => _communicator.PreSettingsPanelDraw.Unsubscribe(value!); - } - - public event Action? PostEnabledDraw - { - add => _communicator.PostEnabledDraw.Subscribe(value!, Communication.PostEnabledDraw.Priority.Default); - remove => _communicator.PostEnabledDraw.Unsubscribe(value!); - } - - public event Action? PostSettingsPanelDraw - { - add => _communicator.PostSettingsPanelDraw.Subscribe(value!, Communication.PostSettingsPanelDraw.Priority.Default); - remove => _communicator.PostSettingsPanelDraw.Unsubscribe(value!); - } - - public event GameObjectRedrawnDelegate? GameObjectRedrawn - { - add - { - CheckInitialized(); - _redrawService.GameObjectRedrawn += value; - } - remove - { - CheckInitialized(); - _redrawService.GameObjectRedrawn -= value; - } - } - - public event ModSettingChangedDelegate? ModSettingChanged; - - public event CreatingCharacterBaseDelegate? CreatingCharacterBase - { - add - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatingCharacterBase.Subscribe(new Action(value), - Communication.CreatingCharacterBase.Priority.Api); - } - remove - { - if (value == null) - return; - - CheckInitialized(); - _communicator.CreatingCharacterBase.Unsubscribe(new Action(value)); - } - } - - public event CreatedCharacterBaseDelegate? CreatedCharacterBase; - - public bool Valid - => _lumina != null; - - private CommunicatorService _communicator; - private Lumina.GameData? _lumina; - - private IDataManager _gameData; - private IFramework _framework; - private ObjectManager _objects; - private ModManager _modManager; - private ResourceLoader _resourceLoader; - private Configuration _config; - private CollectionManager _collectionManager; - private TempCollectionManager _tempCollections; - private TempModManager _tempMods; - private ActorManager _actors; - private CollectionResolver _collectionResolver; - private CutsceneService _cutsceneService; - private ModImportManager _modImportManager; - private CollectionEditor _collectionEditor; - private RedrawService _redrawService; - private ModFileSystem _modFileSystem; - private ConfigWindow _configWindow; - private TextureManager _textureManager; - private ResourceTreeFactory _resourceTreeFactory; - - public unsafe PenumbraApi(CommunicatorService communicator, IDataManager gameData, IFramework framework, ObjectManager objects, - ModManager modManager, ResourceLoader resourceLoader, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, TempModManager tempMods, ActorManager actors, CollectionResolver collectionResolver, - CutsceneService cutsceneService, ModImportManager modImportManager, CollectionEditor collectionEditor, RedrawService redrawService, - ModFileSystem modFileSystem, ConfigWindow configWindow, TextureManager textureManager, ResourceTreeFactory resourceTreeFactory) - { - _communicator = communicator; - _gameData = gameData; - _framework = framework; - _objects = objects; - _modManager = modManager; - _resourceLoader = resourceLoader; - _config = config; - _collectionManager = collectionManager; - _tempCollections = tempCollections; - _tempMods = tempMods; - _actors = actors; - _collectionResolver = collectionResolver; - _cutsceneService = cutsceneService; - _modImportManager = modImportManager; - _collectionEditor = collectionEditor; - _redrawService = redrawService; - _modFileSystem = modFileSystem; - _configWindow = configWindow; - _textureManager = textureManager; - _resourceTreeFactory = resourceTreeFactory; - _lumina = gameData.GameData; - - _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber, ModPathChanged.Priority.Api); - _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); - _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); - _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); - _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api); - } - - public unsafe void Dispose() - { - if (!Valid) - return; - - _resourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); - _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); - _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); - _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited); - _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); - _lumina = null; - _communicator = null!; - _modManager = null!; - _resourceLoader = null!; - _config = null!; - _collectionManager = null!; - _tempCollections = null!; - _tempMods = null!; - _actors = null!; - _collectionResolver = null!; - _cutsceneService = null!; - _modImportManager = null!; - _collectionEditor = null!; - _redrawService = null!; - _modFileSystem = null!; - _configWindow = null!; - _textureManager = null!; - _resourceTreeFactory = null!; - _framework = null!; - } - - public event ChangedItemClick? ChangedItemClicked - { - add => _communicator.ChangedItemClick.Subscribe(new Action(value!), - Communication.ChangedItemClick.Priority.Default); - remove => _communicator.ChangedItemClick.Unsubscribe(new Action(value!)); - } - - public string GetModDirectory() - { - CheckInitialized(); - return _config.ModDirectory; - } - - private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, - ResolveData resolveData) - { - if (resolveData.AssociatedGameObject != nint.Zero) - GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString()); - } - - public event Action? ModDirectoryChanged - { - add - { - CheckInitialized(); - _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); - } - remove - { - CheckInitialized(); - _communicator.ModDirectoryChanged.Unsubscribe(value!); - } - } - - public bool GetEnabledState() - => _config.EnableMods; - - public event Action? EnabledChange - { - add - { - CheckInitialized(); - _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); - } - remove - { - CheckInitialized(); - _communicator.EnabledChanged.Unsubscribe(value!); - } - } - - public string GetConfiguration() - { - CheckInitialized(); - return JsonConvert.SerializeObject(_config, Formatting.Indented); - } - - public event ChangedItemHover? ChangedItemTooltip - { - add => _communicator.ChangedItemHover.Subscribe(new Action(value!), Communication.ChangedItemHover.Priority.Default); - remove => _communicator.ChangedItemHover.Unsubscribe(new Action(value!)); - } - - public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved; - - public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) - { - CheckInitialized(); - if (_configWindow == null) - return PenumbraApiEc.SystemDisposed; - - _configWindow.IsOpen = true; - - if (!Enum.IsDefined(tab)) - return PenumbraApiEc.InvalidArgument; - - if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) - { - if (_modManager.TryGetMod(modDirectory, modName, out var mod)) - _communicator.SelectTab.Invoke(tab, mod); - else - return PenumbraApiEc.ModMissing; - } - else if (tab != TabType.None) - { - _communicator.SelectTab.Invoke(tab, null); - } - - return PenumbraApiEc.Success; - } - - public void CloseMainWindow() - { - CheckInitialized(); - if (_configWindow == null) - return; - - _configWindow.IsOpen = false; - } - - public void RedrawObject(int tableIndex, RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawObject(tableIndex, setting); - } - - public void RedrawObject(string name, RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawObject(name, setting); - } - - public void RedrawObject(GameObject? gameObject, RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawObject(gameObject, setting); - } - - public void RedrawAll(RedrawType setting) - { - CheckInitialized(); - _redrawService.RedrawAll(setting); - } - - public string ResolveDefaultPath(string path) - { - CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Active.Default); - } - - public string ResolveInterfacePath(string path) - { - CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Active.Interface); - } - - public string ResolvePlayerPath(string path) - { - CheckInitialized(); - return ResolvePath(path, _modManager, _collectionResolver.PlayerCollection()); - } - - // TODO: cleanup when incrementing API level - public string ResolvePath(string path, string characterName) - => ResolvePath(path, characterName, ushort.MaxValue); - - public string ResolveGameObjectPath(string path, int gameObjectIdx) - { - CheckInitialized(); - AssociatedCollection(gameObjectIdx, out var collection); - return ResolvePath(path, _modManager, collection); - } - - public string ResolvePath(string path, string characterName, ushort worldId) - { - CheckInitialized(); - return ResolvePath(path, _modManager, - _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId))); - } - - // TODO: cleanup when incrementing API level - public string[] ReverseResolvePath(string path, string characterName) - => ReverseResolvePath(path, characterName, ushort.MaxValue); - - public string[] ReverseResolvePath(string path, string characterName, ushort worldId) - { - CheckInitialized(); - if (!_config.EnableMods) - return [path]; - - var ret = _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); - return ret.Select(r => r.ToString()).ToArray(); - } - - public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx) - { - CheckInitialized(); - if (!_config.EnableMods) - return [path]; - - AssociatedCollection(gameObjectIdx, out var collection); - var ret = collection.ReverseResolvePath(new FullPath(path)); - return ret.Select(r => r.ToString()).ToArray(); - } - - public string[] ReverseResolvePlayerPath(string path) - { - CheckInitialized(); - if (!_config.EnableMods) - return [path]; - - var ret = _collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); - return ret.Select(r => r.ToString()).ToArray(); - } - - public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) - { - CheckInitialized(); - if (!_config.EnableMods) - return (forward, reverse.Select(p => new[] - { - p, - }).ToArray()); - - var playerCollection = _collectionResolver.PlayerCollection(); - var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray(); - var reverseResolved = playerCollection.ReverseResolvePaths(reverse); - return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); - } - - public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) - { - CheckInitialized(); - if (!_config.EnableMods) - return (forward, reverse.Select(p => new[] - { - p, - }).ToArray()); - - return await Task.Run(async () => - { - var playerCollection = await _framework.RunOnFrameworkThread(_collectionResolver.PlayerCollection).ConfigureAwait(false); - var forwardTask = Task.Run(() => - { - var forwardRet = new string[forward.Length]; - Parallel.For(0, forward.Length, idx => forwardRet[idx] = ResolvePath(forward[idx], _modManager, playerCollection)); - return forwardRet; - }).ConfigureAwait(false); - var reverseTask = Task.Run(() => playerCollection.ReverseResolvePaths(reverse)).ConfigureAwait(false); - var reverseResolved = (await reverseTask).Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); - return (await forwardTask, reverseResolved); - }); - } - - public T? GetFile(string gamePath) where T : FileResource - => GetFileIntern(ResolveDefaultPath(gamePath)); - - public T? GetFile(string gamePath, string characterName) where T : FileResource - => GetFileIntern(ResolvePath(gamePath, characterName)); - - public IReadOnlyDictionary GetChangedItemsForCollection(string collectionName) - { - CheckInitialized(); - try - { - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - collection = ModCollection.Empty; - - if (collection.HasCache) - return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); - - Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded."); - return new Dictionary(); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not obtain Changed Items for {collectionName}:\n{e}"); - throw; - } - } - - public string GetCollectionForType(ApiCollectionType type) - { - CheckInitialized(); - if (!Enum.IsDefined(type)) - return string.Empty; - - var collection = _collectionManager.Active.ByType((CollectionType)type); - return collection?.Name ?? string.Empty; - } - - public (PenumbraApiEc, string OldCollection) SetCollectionForType(ApiCollectionType type, string collectionName, bool allowCreateNew, - bool allowDelete) - { - CheckInitialized(); - if (!Enum.IsDefined(type)) - return (PenumbraApiEc.InvalidArgument, string.Empty); - - var oldCollection = _collectionManager.Active.ByType((CollectionType)type)?.Name ?? string.Empty; - - if (collectionName.Length == 0) - { - if (oldCollection.Length == 0) - return (PenumbraApiEc.NothingChanged, oldCollection); - - if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) - return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - - _collectionManager.Active.RemoveSpecialCollection((CollectionType)type); - return (PenumbraApiEc.Success, oldCollection); - } - - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return (PenumbraApiEc.CollectionMissing, oldCollection); - - if (oldCollection.Length == 0) - { - if (!allowCreateNew) - return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - - _collectionManager.Active.CreateSpecialCollection((CollectionType)type); - } - else if (oldCollection == collection.Name) - { - return (PenumbraApiEc.NothingChanged, oldCollection); - } - - _collectionManager.Active.SetCollection(collection, (CollectionType)type); - return (PenumbraApiEc.Success, oldCollection); - } - - public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject(int gameObjectIdx) - { - CheckInitialized(); - var id = AssociatedIdentifier(gameObjectIdx); - if (!id.IsValid) - return (false, false, _collectionManager.Active.Default.Name); - - if (_collectionManager.Active.Individuals.TryGetValue(id, out var collection)) - return (true, true, collection.Name); - - AssociatedCollection(gameObjectIdx, out collection); - return (true, false, collection.Name); - } - - public (PenumbraApiEc, string OldCollection) SetCollectionForObject(int gameObjectIdx, string collectionName, bool allowCreateNew, - bool allowDelete) - { - CheckInitialized(); - var id = AssociatedIdentifier(gameObjectIdx); - if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Active.Default.Name); - - var oldCollection = _collectionManager.Active.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; - - if (collectionName.Length == 0) - { - if (oldCollection.Length == 0) - return (PenumbraApiEc.NothingChanged, oldCollection); - - if (!allowDelete) - return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - - var idx = _collectionManager.Active.Individuals.Index(id); - _collectionManager.Active.RemoveIndividualCollection(idx); - return (PenumbraApiEc.Success, oldCollection); - } - - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return (PenumbraApiEc.CollectionMissing, oldCollection); - - if (oldCollection.Length == 0) - { - if (!allowCreateNew) - return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - - var ids = _collectionManager.Active.Individuals.GetGroup(id); - _collectionManager.Active.CreateIndividualCollection(ids); - } - else if (oldCollection == collection.Name) - { - return (PenumbraApiEc.NothingChanged, oldCollection); - } - - _collectionManager.Active.SetCollection(collection, CollectionType.Individual, _collectionManager.Active.Individuals.Index(id)); - return (PenumbraApiEc.Success, oldCollection); - } - - public IList GetCollections() - { - CheckInitialized(); - return _collectionManager.Storage.Select(c => c.Name).ToArray(); - } - - public string GetCurrentCollection() - { - CheckInitialized(); - return _collectionManager.Active.Current.Name; - } - - public string GetDefaultCollection() - { - CheckInitialized(); - return _collectionManager.Active.Default.Name; - } - - public string GetInterfaceCollection() - { - CheckInitialized(); - return _collectionManager.Active.Interface.Name; - } - - // TODO: cleanup when incrementing API level - public (string, bool) GetCharacterCollection(string characterName) - => GetCharacterCollection(characterName, ushort.MaxValue); - - public (string, bool) GetCharacterCollection(string characterName, ushort worldId) - { - CheckInitialized(); - return _collectionManager.Active.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) - ? (collection.Name, true) - : (_collectionManager.Active.Default.Name, false); - } - - public unsafe (nint, string) GetDrawObjectInfo(nint drawObject) - { - CheckInitialized(); - var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return (data.AssociatedGameObject, data.ModCollection.Name); - } - - public int GetCutsceneParentIndex(int actorIdx) - { - CheckInitialized(); - return _cutsceneService.GetParentIndex(actorIdx); - } - - public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) - { - CheckInitialized(); - if (_cutsceneService.SetParentIndex(copyIdx, newParentIdx)) - return PenumbraApiEc.Success; - - return PenumbraApiEc.InvalidArgument; - } - - public IList<(string, string)> GetModList() - { - CheckInitialized(); - return _modManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); - } - - public IDictionary, GroupType)>? GetAvailableModSettings(string modDirectory, string modName) - { - CheckInitialized(); - return _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => ((IList)g.Select(o => o.Name).ToList(), g.Type)) - : null; - } - - public (PenumbraApiEc, (bool, int, IDictionary>, bool)?) GetCurrentModSettings(string collectionName, - string modDirectory, string modName, bool allowInheritance) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return (PenumbraApiEc.CollectionMissing, null); - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return (PenumbraApiEc.ModMissing, null); - - var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; - if (settings == null) - return (PenumbraApiEc.Success, null); - - var shareSettings = settings.ConvertToShareable(mod); - return (PenumbraApiEc.Success, - (shareSettings.Enabled, shareSettings.Priority.Value, shareSettings.Settings, collection.Settings[mod.Index] != null)); - } - - public PenumbraApiEc ReloadMod(string modDirectory, string modName) - { - CheckInitialized(); - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.ModMissing, Args("ModDirectory", modDirectory, "ModName", modName)); - - _modManager.ReloadMod(mod); - return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory, "ModName", modName)); - } - - public PenumbraApiEc InstallMod(string modFilePackagePath) - { - if (File.Exists(modFilePackagePath)) - { - _modImportManager.AddUnpack(modFilePackagePath); - return Return(PenumbraApiEc.Success, Args("ModFilePackagePath", modFilePackagePath)); - } - else - { - return Return(PenumbraApiEc.FileMissing, Args("ModFilePackagePath", modFilePackagePath)); - } - } - - public PenumbraApiEc AddMod(string modDirectory) - { - CheckInitialized(); - var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); - if (!dir.Exists) - return Return(PenumbraApiEc.FileMissing, Args("ModDirectory", modDirectory)); - - - _modManager.AddMod(dir); - if (_config.UseFileSystemCompression) - new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), - CompressionAlgorithm.Xpress8K); - return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory)); - } - - public PenumbraApiEc DeleteMod(string modDirectory, string modName) - { - CheckInitialized(); - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.NothingChanged, Args("ModDirectory", modDirectory, "ModName", modName)); - - _modManager.DeleteMod(mod); - return Return(PenumbraApiEc.Success, Args("ModDirectory", modDirectory, "ModName", modName)); - } - - public event Action? ModDeleted; - public event Action? ModAdded; - public event Action? ModMoved; - - private void ModPathChangeSubscriber(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) - { - switch (type) - { - case ModPathChangeType.Reloaded: - TriggerSettingEdited(mod); - 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; - } - } - - public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName) - { - CheckInitialized(); - if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) - return (PenumbraApiEc.ModMissing, string.Empty, false); - - var fullPath = leaf.FullName(); - - return (PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath(mod, fullPath)); - } - - public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) - { - CheckInitialized(); - if (newPath.Length == 0) - return PenumbraApiEc.InvalidArgument; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) - return PenumbraApiEc.ModMissing; - - try - { - _modFileSystem.RenameAndMove(leaf, newPath); - return PenumbraApiEc.Success; - } - catch - { - return PenumbraApiEc.PathRenameFailed; - } - } - - public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; - - - return _collectionEditor.SetModInheritance(collection, mod, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; - - return _collectionEditor.SetModState(collection, mod, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return PenumbraApiEc.ModMissing; - - return _collectionEditor.SetModPriority(collection, mod, new ModPriority(priority)) - ? PenumbraApiEc.Success - : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName, - string optionName) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return Return(PenumbraApiEc.CollectionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.ModMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); - if (groupIdx < 0) - return Return(PenumbraApiEc.OptionGroupMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); - if (optionIdx < 0) - return Return(PenumbraApiEc.OptionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - - var setting = mod.Groups[groupIdx].Type switch - { - GroupType.Multi => Setting.Multi(optionIdx), - GroupType.Single => Setting.Single(optionIdx), - _ => Setting.Zero, - }; - - return Return( - _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "OptionName", optionName)); - } - - public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName, - IReadOnlyList optionNames) - { - CheckInitialized(); - if (!_collectionManager.Storage.ByName(collectionName, out var collection)) - return Return(PenumbraApiEc.CollectionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return Return(PenumbraApiEc.ModMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); - if (groupIdx < 0) - return Return(PenumbraApiEc.OptionGroupMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - var group = mod.Groups[groupIdx]; - - var setting = Setting.Zero; - if (group.Type == GroupType.Single) - { - var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return Return(PenumbraApiEc.OptionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - - setting = Setting.Single(optionIdx); - } - else - { - foreach (var name in optionNames) - { - var optionIdx = group.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return Return(PenumbraApiEc.OptionMissing, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", - optionGroupName, "#optionNames", optionNames.Count.ToString())); - - setting |= Setting.Multi(optionIdx); - } - } - - return Return( - _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, - Args("CollectionName", collectionName, "ModDirectory", modDirectory, "ModName", modName, "OptionGroupName", optionGroupName, - "#optionNames", optionNames.Count.ToString())); - } - - - public PenumbraApiEc CopyModSettings(string? collectionName, string modDirectoryFrom, string modDirectoryTo) - { - CheckInitialized(); - - var sourceMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase)); - var targetMod = _modManager.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase)); - if (string.IsNullOrEmpty(collectionName)) - foreach (var collection in _collectionManager.Storage) - _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); - else if (_collectionManager.Storage.ByName(collectionName, out var collection)) - _collectionEditor.CopyModSettings(collection, sourceMod, modDirectoryFrom, targetMod, modDirectoryTo); - else - return PenumbraApiEc.CollectionMissing; - - return PenumbraApiEc.Success; - } - - public (PenumbraApiEc, string) CreateTemporaryCollection(string tag, string character, bool forceOverwriteCharacter) - { - CheckInitialized(); - - if (!ActorIdentifierFactory.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) - return (PenumbraApiEc.InvalidArgument, string.Empty); - - var identifier = NameToIdentifier(character, ushort.MaxValue); - if (!identifier.IsValid) - return (PenumbraApiEc.InvalidArgument, string.Empty); - - if (!forceOverwriteCharacter && _collectionManager.Active.Individuals.ContainsKey(identifier) - || _tempCollections.Collections.ContainsKey(identifier)) - return (PenumbraApiEc.CharacterCollectionExists, string.Empty); - - var name = $"{tag}_{character}"; - var ret = CreateNamedTemporaryCollection(name); - if (ret != PenumbraApiEc.Success) - return (ret, name); - - if (_tempCollections.AddIdentifier(name, identifier)) - return (PenumbraApiEc.Success, name); - - _tempCollections.RemoveTemporaryCollection(name); - return (PenumbraApiEc.UnknownError, string.Empty); - } - - public PenumbraApiEc CreateNamedTemporaryCollection(string name) - { - CheckInitialized(); - if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name, _config.ReplaceNonAsciiOnImport) != name || name.Contains('|')) - return PenumbraApiEc.InvalidArgument; - - return _tempCollections.CreateTemporaryCollection(name).Length > 0 - ? PenumbraApiEc.Success - : PenumbraApiEc.CollectionExists; - } - - public PenumbraApiEc AssignTemporaryCollection(string collectionName, int actorIndex, bool forceAssignment) - { - CheckInitialized(); - - if (actorIndex < 0 || actorIndex >= _objects.TotalCount) - return PenumbraApiEc.InvalidArgument; - - var identifier = _actors.FromObject(_objects[actorIndex], out _, false, false, true); - if (!identifier.IsValid) - return PenumbraApiEc.InvalidArgument; - - if (!_tempCollections.CollectionByName(collectionName, out var collection)) - return PenumbraApiEc.CollectionMissing; - - if (forceAssignment) - { - if (_tempCollections.Collections.ContainsKey(identifier) && !_tempCollections.Collections.Delete(identifier)) - return PenumbraApiEc.AssignmentDeletionFailed; - } - else if (_tempCollections.Collections.ContainsKey(identifier) - || _collectionManager.Active.Individuals.ContainsKey(identifier)) - { - return PenumbraApiEc.CharacterCollectionExists; - } - - var group = _tempCollections.Collections.GetGroup(identifier); - return _tempCollections.AddIdentifier(collection, group) - ? PenumbraApiEc.Success - : PenumbraApiEc.UnknownError; - } - - public PenumbraApiEc RemoveTemporaryCollection(string character) - { - CheckInitialized(); - return _tempCollections.RemoveByCharacterName(character) - ? PenumbraApiEc.Success - : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc RemoveTemporaryCollectionByName(string name) - { - CheckInitialized(); - return _tempCollections.RemoveTemporaryCollection(name) - ? PenumbraApiEc.Success - : PenumbraApiEc.NothingChanged; - } - - public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary paths, string manipString, int priority) - { - CheckInitialized(); - if (!ConvertPaths(paths, out var p)) - return PenumbraApiEc.InvalidGamePath; - - if (!ConvertManips(manipString, out var m)) - return PenumbraApiEc.InvalidManipulation; - - return _tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - _ => PenumbraApiEc.UnknownError, - }; - } - - public PenumbraApiEc AddTemporaryMod(string tag, string collectionName, Dictionary paths, string manipString, - int priority) - { - CheckInitialized(); - if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.Storage.ByName(collectionName, out collection)) - return PenumbraApiEc.CollectionMissing; - - if (!ConvertPaths(paths, out var p)) - return PenumbraApiEc.InvalidGamePath; - - if (!ConvertManips(manipString, out var m)) - return PenumbraApiEc.InvalidManipulation; - - return _tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - _ => PenumbraApiEc.UnknownError, - }; - } - - public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) - { - CheckInitialized(); - return _tempMods.Unregister(tag, null, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, - _ => PenumbraApiEc.UnknownError, - }; - } - - public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) - { - CheckInitialized(); - if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.Storage.ByName(collectionName, out collection)) - return PenumbraApiEc.CollectionMissing; - - return _tempMods.Unregister(tag, collection, new ModPriority(priority)) switch - { - RedirectResult.Success => PenumbraApiEc.Success, - RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, - _ => PenumbraApiEc.UnknownError, - }; - } - - public string GetPlayerMetaManipulations() - { - CheckInitialized(); - var collection = _collectionResolver.PlayerCollection(); - var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); - } - - public Task ConvertTextureFile(string inputFile, string outputFile, TextureType textureType, bool mipMaps) - => textureType switch - { - TextureType.Png => _textureManager.SavePng(inputFile, outputFile), - TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), - TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), - TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), - TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, inputFile, outputFile), - TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, inputFile, outputFile), - TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), - TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), - TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), - _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), - }; - - // @formatter:off - public Task ConvertTextureData(byte[] rgbaData, int width, string outputFile, TextureType textureType, bool mipMaps) - => textureType switch - { - TextureType.Png => _textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.AsIsTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.AsIsDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.RgbaTex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.RgbaDds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc3Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc3Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc7Tex => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - TextureType.Bc7Dds => _textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), - _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), - }; - // @formatter:on - - public IReadOnlyDictionary?[] GetGameObjectResourcePaths(ushort[] gameObjects) - { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, 0); - var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); - - return Array.ConvertAll(gameObjects, obj => pathDictionaries.GetValueOrDefault(obj)); - } - - public IReadOnlyDictionary> GetPlayerResourcePaths() - { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly); - var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); - - return pathDictionaries.AsReadOnly(); - } - - public IReadOnlyDictionary?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, - params ushort[] gameObjects) - { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); - var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); - - return Array.ConvertAll(gameObjects, obj => resDictionaries.GetValueOrDefault(obj)); - } - - public IReadOnlyDictionary> GetPlayerResourcesOfType(ResourceType type, - bool withUiData) - { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); - var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); - - return resDictionaries.AsReadOnly(); - } - - public Ipc.ResourceTree?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) - { - var characters = gameObjects.Select(index => _objects.GetDalamudObject((int)index)).OfType(); - var resourceTrees = _resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); - var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); - - return Array.ConvertAll(gameObjects, obj => resDictionary.GetValueOrDefault(obj)); - } - - public IReadOnlyDictionary GetPlayerResourceTrees(bool withUiData) - { - var resourceTrees = _resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly - | (withUiData ? ResourceTreeFactory.Flags.WithUiData : 0)); - var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); - - return resDictionary.AsReadOnly(); - } - - // TODO: cleanup when incrementing API - public string GetMetaManipulations(string characterName) - => GetMetaManipulations(characterName, ushort.MaxValue); - - public string GetMetaManipulations(string characterName, ushort worldId) - { - CheckInitialized(); - var identifier = NameToIdentifier(characterName, worldId); - var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) - ? c - : _collectionManager.Active.Individual(identifier); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); - } - - public string GetGameObjectMetaManipulations(int gameObjectIdx) - { - CheckInitialized(); - AssociatedCollection(gameObjectIdx, out var collection); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void CheckInitialized() - { - if (!Valid) - throw new Exception("PluginShare is not initialized."); - } - - // Return the collection associated to a current game object. If it does not exist, return the default collection. - // If the index is invalid, returns false and the default collection. - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) - { - collection = _collectionManager.Active.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) - return false; - - var ptr = _objects[gameObjectIdx]; - var data = _collectionResolver.IdentifyCollection(ptr.AsObject, false); - if (data.Valid) - collection = data.ModCollection; - - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ActorIdentifier AssociatedIdentifier(int gameObjectIdx) - { - if (gameObjectIdx < 0 || gameObjectIdx >= _objects.TotalCount) - return ActorIdentifier.Invalid; - - var ptr = _objects[gameObjectIdx]; - return _actors.FromObject(ptr, out _, false, true, true); - } - - // Resolve a path given by string for a specific collection. - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private string ResolvePath(string path, ModManager _, ModCollection collection) - { - if (!_config.EnableMods) - return path; - - var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; - var ret = collection.ResolvePath(gamePath); - return ret?.ToString() ?? path; - } - - // Get a file for a resolved path. - private T? GetFileIntern(string resolvedPath) where T : FileResource - { - CheckInitialized(); - try - { - return Path.IsPathRooted(resolvedPath) - ? _lumina?.GetFileFromDisk(resolvedPath) - : _gameData.GetFile(resolvedPath); - } - catch (Exception e) - { - Penumbra.Log.Warning($"Could not load file {resolvedPath}:\n{e}"); - return null; - } - } - - - // Convert a dictionary of strings to a dictionary of gamepaths to full paths. - // Only returns true if all paths can successfully be converted and added. - private static bool ConvertPaths(IReadOnlyDictionary redirections, - [NotNullWhen(true)] out Dictionary? paths) - { - paths = new Dictionary(redirections.Count); - foreach (var (gString, fString) in redirections) - { - if (!Utf8GamePath.FromString(gString, out var path, false)) - { - paths = null; - return false; - } - - var fullPath = new FullPath(fString); - if (!paths.TryAdd(path, fullPath)) - { - paths = null; - return false; - } - } - - return true; - } - - // Convert manipulations from a transmitted base64 string to actual manipulations. - // The empty string is treated as an empty set. - // Only returns true if all conversions are successful and distinct. - private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out HashSet? manips) - { - if (manipString.Length == 0) - { - manips = new HashSet(); - return true; - } - - if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) - { - manips = null; - return false; - } - - manips = new HashSet(manipArray!.Length); - foreach (var manip in manipArray.Where(m => m.Validate())) - { - if (manips.Add(manip)) - continue; - - Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); - manips = null; - return false; - } - - return true; - } - - // TODO: replace all usages with ActorIdentifier stuff when incrementing API - private ActorIdentifier NameToIdentifier(string name, ushort worldId) - { - // Verified to be valid name beforehand. - var b = ByteString.FromStringUnsafe(name, false); - return _actors.CreatePlayer(b, worldId); - } - - private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, collection.Name, mod?.ModPath.Name ?? string.Empty, inherited); - - private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) - => CreatedCharacterBase?.Invoke(gameObject, collection.Name, drawObject); - - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) - { - switch (type) - { - case ModOptionChangeType.GroupDeleted: - case ModOptionChangeType.GroupMoved: - case ModOptionChangeType.GroupTypeChanged: - case ModOptionChangeType.PriorityChanged: - case ModOptionChangeType.OptionDeleted: - case ModOptionChangeType.OptionMoved: - case ModOptionChangeType.OptionFilesChanged: - case ModOptionChangeType.OptionFilesAdded: - case ModOptionChangeType.OptionSwapsChanged: - case ModOptionChangeType.OptionMetaChanged: - TriggerSettingEdited(mod); - break; - } - } - - private void OnModFileChanged(Mod mod, FileRegistry file) - { - if (file.CurrentUsage == 0) - return; - - TriggerSettingEdited(mod); - } - - private void TriggerSettingEdited(Mod mod) - { - var collection = _collectionResolver.PlayerCollection(); - var (settings, parent) = collection[mod.Index]; - if (settings is { Enabled: true }) - ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Name, mod.Identifier, parent != collection); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static LazyString Args(params string[] arguments) - { - if (arguments.Length == 0) - return new LazyString(() => "no arguments"); - - return new LazyString(() => - { - var sb = new StringBuilder(); - for (var i = 0; i < arguments.Length / 2; ++i) - { - sb.Append(arguments[2 * i]); - sb.Append(" = "); - sb.Append(arguments[2 * i + 1]); - sb.Append(", "); - } - - return sb.ToString(0, sb.Length - 2); - }); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") - { - Penumbra.Log.Debug( - $"[{name}] Called with {args}, returned {ec}."); - return ec; - } -} diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs deleted file mode 100644 index 78887156..00000000 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ /dev/null @@ -1,435 +0,0 @@ -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Plugin; -using Penumbra.GameData.Enums; -using Penumbra.Api.Enums; -using Penumbra.Api.Helpers; -using Penumbra.Collections.Manager; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Api; - -using CurrentSettings = ValueTuple>, bool)?>; - -public class PenumbraIpcProviders : IDisposable -{ - internal readonly IPenumbraApi Api; - - // Plugin State - internal readonly EventProvider Initialized; - internal readonly EventProvider Disposed; - internal readonly FuncProvider ApiVersion; - internal readonly FuncProvider<(int Breaking, int Features)> ApiVersions; - internal readonly FuncProvider GetEnabledState; - internal readonly EventProvider EnabledChange; - - // Configuration - internal readonly FuncProvider GetModDirectory; - internal readonly FuncProvider GetConfiguration; - internal readonly EventProvider ModDirectoryChanged; - - // UI - internal readonly EventProvider PreSettingsTabBarDraw; - internal readonly EventProvider PreSettingsDraw; - internal readonly EventProvider PostEnabledDraw; - internal readonly EventProvider PostSettingsDraw; - internal readonly EventProvider ChangedItemTooltip; - internal readonly EventProvider ChangedItemClick; - internal readonly FuncProvider OpenMainWindow; - internal readonly ActionProvider CloseMainWindow; - - // Redrawing - internal readonly ActionProvider RedrawAll; - internal readonly ActionProvider RedrawObject; - internal readonly ActionProvider RedrawObjectByIndex; - internal readonly ActionProvider RedrawObjectByName; - internal readonly EventProvider GameObjectRedrawn; - - // Game State - internal readonly FuncProvider GetDrawObjectInfo; - internal readonly FuncProvider GetCutsceneParentIndex; - internal readonly FuncProvider SetCutsceneParentIndex; - internal readonly EventProvider CreatingCharacterBase; - internal readonly EventProvider CreatedCharacterBase; - internal readonly EventProvider GameObjectResourcePathResolved; - - // Resolve - internal readonly FuncProvider ResolveDefaultPath; - internal readonly FuncProvider ResolveInterfacePath; - internal readonly FuncProvider ResolvePlayerPath; - internal readonly FuncProvider ResolveGameObjectPath; - internal readonly FuncProvider ResolveCharacterPath; - internal readonly FuncProvider ReverseResolvePath; - internal readonly FuncProvider ReverseResolveGameObjectPath; - internal readonly FuncProvider ReverseResolvePlayerPath; - internal readonly FuncProvider ResolvePlayerPaths; - internal readonly FuncProvider> ResolvePlayerPathsAsync; - - // Collections - internal readonly FuncProvider> GetCollections; - internal readonly FuncProvider GetCurrentCollectionName; - internal readonly FuncProvider GetDefaultCollectionName; - internal readonly FuncProvider GetInterfaceCollectionName; - internal readonly FuncProvider GetCharacterCollectionName; - internal readonly FuncProvider GetCollectionForType; - internal readonly FuncProvider SetCollectionForType; - internal readonly FuncProvider GetCollectionForObject; - internal readonly FuncProvider SetCollectionForObject; - internal readonly FuncProvider> GetChangedItems; - - // Meta - internal readonly FuncProvider GetPlayerMetaManipulations; - internal readonly FuncProvider GetMetaManipulations; - internal readonly FuncProvider GetGameObjectMetaManipulations; - - // Mods - internal readonly FuncProvider> GetMods; - internal readonly FuncProvider ReloadMod; - internal readonly FuncProvider InstallMod; - internal readonly FuncProvider AddMod; - internal readonly FuncProvider DeleteMod; - internal readonly FuncProvider GetModPath; - internal readonly FuncProvider SetModPath; - internal readonly EventProvider ModDeleted; - internal readonly EventProvider ModAdded; - internal readonly EventProvider ModMoved; - - // ModSettings - internal readonly FuncProvider, GroupType)>?> GetAvailableModSettings; - internal readonly FuncProvider GetCurrentModSettings; - internal readonly FuncProvider TryInheritMod; - internal readonly FuncProvider TrySetMod; - internal readonly FuncProvider TrySetModPriority; - internal readonly FuncProvider TrySetModSetting; - internal readonly FuncProvider, PenumbraApiEc> TrySetModSettings; - internal readonly EventProvider ModSettingChanged; - internal readonly FuncProvider CopyModSettings; - - // Editing - internal readonly FuncProvider ConvertTextureFile; - internal readonly FuncProvider ConvertTextureData; - - // Temporary - internal readonly FuncProvider CreateTemporaryCollection; - internal readonly FuncProvider RemoveTemporaryCollection; - internal readonly FuncProvider CreateNamedTemporaryCollection; - internal readonly FuncProvider RemoveTemporaryCollectionByName; - internal readonly FuncProvider AssignTemporaryCollection; - internal readonly FuncProvider, string, int, PenumbraApiEc> AddTemporaryModAll; - internal readonly FuncProvider, string, int, PenumbraApiEc> AddTemporaryMod; - internal readonly FuncProvider RemoveTemporaryModAll; - internal readonly FuncProvider RemoveTemporaryMod; - - // Resource Tree - internal readonly FuncProvider?[]> GetGameObjectResourcePaths; - internal readonly FuncProvider>> GetPlayerResourcePaths; - - internal readonly FuncProvider?[]> - GetGameObjectResourcesOfType; - - internal readonly - FuncProvider>> - GetPlayerResourcesOfType; - - internal readonly FuncProvider GetGameObjectResourceTrees; - internal readonly FuncProvider> GetPlayerResourceTrees; - - public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections, - TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService, Configuration config) - { - Api = api; - - // Plugin State - Initialized = Ipc.Initialized.Provider(pi); - Disposed = Ipc.Disposed.Provider(pi); - ApiVersion = Ipc.ApiVersion.Provider(pi, DeprecatedVersion); - ApiVersions = Ipc.ApiVersions.Provider(pi, () => Api.ApiVersion); - GetEnabledState = Ipc.GetEnabledState.Provider(pi, Api.GetEnabledState); - EnabledChange = - Ipc.EnabledChange.Provider(pi, () => Api.EnabledChange += EnabledChangeEvent, () => Api.EnabledChange -= EnabledChangeEvent); - - // Configuration - GetModDirectory = Ipc.GetModDirectory.Provider(pi, Api.GetModDirectory); - GetConfiguration = Ipc.GetConfiguration.Provider(pi, Api.GetConfiguration); - ModDirectoryChanged = Ipc.ModDirectoryChanged.Provider(pi, a => Api.ModDirectoryChanged += a, a => Api.ModDirectoryChanged -= a); - - // UI - PreSettingsTabBarDraw = - Ipc.PreSettingsTabBarDraw.Provider(pi, a => Api.PreSettingsTabBarDraw += a, a => Api.PreSettingsTabBarDraw -= a); - PreSettingsDraw = Ipc.PreSettingsDraw.Provider(pi, a => Api.PreSettingsPanelDraw += a, a => Api.PreSettingsPanelDraw -= a); - PostEnabledDraw = - Ipc.PostEnabledDraw.Provider(pi, a => Api.PostEnabledDraw += a, a => Api.PostEnabledDraw -= a); - PostSettingsDraw = Ipc.PostSettingsDraw.Provider(pi, a => Api.PostSettingsPanelDraw += a, a => Api.PostSettingsPanelDraw -= a); - ChangedItemTooltip = - Ipc.ChangedItemTooltip.Provider(pi, () => Api.ChangedItemTooltip += OnTooltip, () => Api.ChangedItemTooltip -= OnTooltip); - ChangedItemClick = Ipc.ChangedItemClick.Provider(pi, () => Api.ChangedItemClicked += OnClick, () => Api.ChangedItemClicked -= OnClick); - OpenMainWindow = Ipc.OpenMainWindow.Provider(pi, Api.OpenMainWindow); - CloseMainWindow = Ipc.CloseMainWindow.Provider(pi, Api.CloseMainWindow); - - // Redrawing - RedrawAll = Ipc.RedrawAll.Provider(pi, Api.RedrawAll); - RedrawObject = Ipc.RedrawObject.Provider(pi, Api.RedrawObject); - RedrawObjectByIndex = Ipc.RedrawObjectByIndex.Provider(pi, Api.RedrawObject); - RedrawObjectByName = Ipc.RedrawObjectByName.Provider(pi, Api.RedrawObject); - GameObjectRedrawn = Ipc.GameObjectRedrawn.Provider(pi, () => Api.GameObjectRedrawn += OnGameObjectRedrawn, - () => Api.GameObjectRedrawn -= OnGameObjectRedrawn); - - // Game State - GetDrawObjectInfo = Ipc.GetDrawObjectInfo.Provider(pi, Api.GetDrawObjectInfo); - GetCutsceneParentIndex = Ipc.GetCutsceneParentIndex.Provider(pi, Api.GetCutsceneParentIndex); - SetCutsceneParentIndex = Ipc.SetCutsceneParentIndex.Provider(pi, Api.SetCutsceneParentIndex); - CreatingCharacterBase = Ipc.CreatingCharacterBase.Provider(pi, - () => Api.CreatingCharacterBase += CreatingCharacterBaseEvent, - () => Api.CreatingCharacterBase -= CreatingCharacterBaseEvent); - CreatedCharacterBase = Ipc.CreatedCharacterBase.Provider(pi, - () => Api.CreatedCharacterBase += CreatedCharacterBaseEvent, - () => Api.CreatedCharacterBase -= CreatedCharacterBaseEvent); - GameObjectResourcePathResolved = Ipc.GameObjectResourcePathResolved.Provider(pi, - () => Api.GameObjectResourceResolved += GameObjectResourceResolvedEvent, - () => Api.GameObjectResourceResolved -= GameObjectResourceResolvedEvent); - - // Resolve - ResolveDefaultPath = Ipc.ResolveDefaultPath.Provider(pi, Api.ResolveDefaultPath); - ResolveInterfacePath = Ipc.ResolveInterfacePath.Provider(pi, Api.ResolveInterfacePath); - ResolvePlayerPath = Ipc.ResolvePlayerPath.Provider(pi, Api.ResolvePlayerPath); - ResolveGameObjectPath = Ipc.ResolveGameObjectPath.Provider(pi, Api.ResolveGameObjectPath); - ResolveCharacterPath = Ipc.ResolveCharacterPath.Provider(pi, Api.ResolvePath); - ReverseResolvePath = Ipc.ReverseResolvePath.Provider(pi, Api.ReverseResolvePath); - ReverseResolveGameObjectPath = Ipc.ReverseResolveGameObjectPath.Provider(pi, Api.ReverseResolveGameObjectPath); - ReverseResolvePlayerPath = Ipc.ReverseResolvePlayerPath.Provider(pi, Api.ReverseResolvePlayerPath); - ResolvePlayerPaths = Ipc.ResolvePlayerPaths.Provider(pi, Api.ResolvePlayerPaths); - ResolvePlayerPathsAsync = Ipc.ResolvePlayerPathsAsync.Provider(pi, Api.ResolvePlayerPathsAsync); - - // Collections - GetCollections = Ipc.GetCollections.Provider(pi, Api.GetCollections); - GetCurrentCollectionName = Ipc.GetCurrentCollectionName.Provider(pi, Api.GetCurrentCollection); - GetDefaultCollectionName = Ipc.GetDefaultCollectionName.Provider(pi, Api.GetDefaultCollection); - GetInterfaceCollectionName = Ipc.GetInterfaceCollectionName.Provider(pi, Api.GetInterfaceCollection); - GetCharacterCollectionName = Ipc.GetCharacterCollectionName.Provider(pi, Api.GetCharacterCollection); - GetCollectionForType = Ipc.GetCollectionForType.Provider(pi, Api.GetCollectionForType); - SetCollectionForType = Ipc.SetCollectionForType.Provider(pi, Api.SetCollectionForType); - GetCollectionForObject = Ipc.GetCollectionForObject.Provider(pi, Api.GetCollectionForObject); - SetCollectionForObject = Ipc.SetCollectionForObject.Provider(pi, Api.SetCollectionForObject); - GetChangedItems = Ipc.GetChangedItems.Provider(pi, Api.GetChangedItemsForCollection); - - // Meta - GetPlayerMetaManipulations = Ipc.GetPlayerMetaManipulations.Provider(pi, Api.GetPlayerMetaManipulations); - GetMetaManipulations = Ipc.GetMetaManipulations.Provider(pi, Api.GetMetaManipulations); - GetGameObjectMetaManipulations = Ipc.GetGameObjectMetaManipulations.Provider(pi, Api.GetGameObjectMetaManipulations); - - // Mods - GetMods = Ipc.GetMods.Provider(pi, Api.GetModList); - ReloadMod = Ipc.ReloadMod.Provider(pi, Api.ReloadMod); - InstallMod = Ipc.InstallMod.Provider(pi, Api.InstallMod); - AddMod = Ipc.AddMod.Provider(pi, Api.AddMod); - DeleteMod = Ipc.DeleteMod.Provider(pi, Api.DeleteMod); - GetModPath = Ipc.GetModPath.Provider(pi, Api.GetModPath); - SetModPath = Ipc.SetModPath.Provider(pi, Api.SetModPath); - ModDeleted = Ipc.ModDeleted.Provider(pi, () => Api.ModDeleted += ModDeletedEvent, () => Api.ModDeleted -= ModDeletedEvent); - ModAdded = Ipc.ModAdded.Provider(pi, () => Api.ModAdded += ModAddedEvent, () => Api.ModAdded -= ModAddedEvent); - ModMoved = Ipc.ModMoved.Provider(pi, () => Api.ModMoved += ModMovedEvent, () => Api.ModMoved -= ModMovedEvent); - - // ModSettings - GetAvailableModSettings = Ipc.GetAvailableModSettings.Provider(pi, Api.GetAvailableModSettings); - GetCurrentModSettings = Ipc.GetCurrentModSettings.Provider(pi, Api.GetCurrentModSettings); - TryInheritMod = Ipc.TryInheritMod.Provider(pi, Api.TryInheritMod); - TrySetMod = Ipc.TrySetMod.Provider(pi, Api.TrySetMod); - TrySetModPriority = Ipc.TrySetModPriority.Provider(pi, Api.TrySetModPriority); - TrySetModSetting = Ipc.TrySetModSetting.Provider(pi, Api.TrySetModSetting); - TrySetModSettings = Ipc.TrySetModSettings.Provider(pi, Api.TrySetModSettings); - ModSettingChanged = Ipc.ModSettingChanged.Provider(pi, - () => Api.ModSettingChanged += ModSettingChangedEvent, - () => Api.ModSettingChanged -= ModSettingChangedEvent); - CopyModSettings = Ipc.CopyModSettings.Provider(pi, Api.CopyModSettings); - - // Editing - ConvertTextureFile = Ipc.ConvertTextureFile.Provider(pi, Api.ConvertTextureFile); - ConvertTextureData = Ipc.ConvertTextureData.Provider(pi, Api.ConvertTextureData); - - // Temporary - CreateTemporaryCollection = Ipc.CreateTemporaryCollection.Provider(pi, Api.CreateTemporaryCollection); - RemoveTemporaryCollection = Ipc.RemoveTemporaryCollection.Provider(pi, Api.RemoveTemporaryCollection); - CreateNamedTemporaryCollection = Ipc.CreateNamedTemporaryCollection.Provider(pi, Api.CreateNamedTemporaryCollection); - RemoveTemporaryCollectionByName = Ipc.RemoveTemporaryCollectionByName.Provider(pi, Api.RemoveTemporaryCollectionByName); - AssignTemporaryCollection = Ipc.AssignTemporaryCollection.Provider(pi, Api.AssignTemporaryCollection); - AddTemporaryModAll = Ipc.AddTemporaryModAll.Provider(pi, Api.AddTemporaryModAll); - AddTemporaryMod = Ipc.AddTemporaryMod.Provider(pi, Api.AddTemporaryMod); - RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll); - RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod); - - // ResourceTree - GetGameObjectResourcePaths = Ipc.GetGameObjectResourcePaths.Provider(pi, Api.GetGameObjectResourcePaths); - GetPlayerResourcePaths = Ipc.GetPlayerResourcePaths.Provider(pi, Api.GetPlayerResourcePaths); - GetGameObjectResourcesOfType = Ipc.GetGameObjectResourcesOfType.Provider(pi, Api.GetGameObjectResourcesOfType); - GetPlayerResourcesOfType = Ipc.GetPlayerResourcesOfType.Provider(pi, Api.GetPlayerResourcesOfType); - GetGameObjectResourceTrees = Ipc.GetGameObjectResourceTrees.Provider(pi, Api.GetGameObjectResourceTrees); - GetPlayerResourceTrees = Ipc.GetPlayerResourceTrees.Provider(pi, Api.GetPlayerResourceTrees); - - Initialized.Invoke(); - } - - public void Dispose() - { - // Plugin State - Initialized.Dispose(); - ApiVersion.Dispose(); - ApiVersions.Dispose(); - GetEnabledState.Dispose(); - EnabledChange.Dispose(); - - // Configuration - GetModDirectory.Dispose(); - GetConfiguration.Dispose(); - ModDirectoryChanged.Dispose(); - - // UI - PreSettingsTabBarDraw.Dispose(); - PreSettingsDraw.Dispose(); - PostEnabledDraw.Dispose(); - PostSettingsDraw.Dispose(); - ChangedItemTooltip.Dispose(); - ChangedItemClick.Dispose(); - OpenMainWindow.Dispose(); - CloseMainWindow.Dispose(); - - // Redrawing - RedrawAll.Dispose(); - RedrawObject.Dispose(); - RedrawObjectByIndex.Dispose(); - RedrawObjectByName.Dispose(); - GameObjectRedrawn.Dispose(); - - // Game State - GetDrawObjectInfo.Dispose(); - GetCutsceneParentIndex.Dispose(); - SetCutsceneParentIndex.Dispose(); - CreatingCharacterBase.Dispose(); - CreatedCharacterBase.Dispose(); - GameObjectResourcePathResolved.Dispose(); - - // Resolve - ResolveDefaultPath.Dispose(); - ResolveInterfacePath.Dispose(); - ResolvePlayerPath.Dispose(); - ResolveGameObjectPath.Dispose(); - ResolveCharacterPath.Dispose(); - ReverseResolvePath.Dispose(); - ReverseResolveGameObjectPath.Dispose(); - ReverseResolvePlayerPath.Dispose(); - ResolvePlayerPaths.Dispose(); - ResolvePlayerPathsAsync.Dispose(); - - // Collections - GetCollections.Dispose(); - GetCurrentCollectionName.Dispose(); - GetDefaultCollectionName.Dispose(); - GetInterfaceCollectionName.Dispose(); - GetCharacterCollectionName.Dispose(); - GetCollectionForType.Dispose(); - SetCollectionForType.Dispose(); - GetCollectionForObject.Dispose(); - SetCollectionForObject.Dispose(); - GetChangedItems.Dispose(); - - // Meta - GetPlayerMetaManipulations.Dispose(); - GetMetaManipulations.Dispose(); - GetGameObjectMetaManipulations.Dispose(); - - // Mods - GetMods.Dispose(); - ReloadMod.Dispose(); - InstallMod.Dispose(); - AddMod.Dispose(); - DeleteMod.Dispose(); - GetModPath.Dispose(); - SetModPath.Dispose(); - ModDeleted.Dispose(); - ModAdded.Dispose(); - ModMoved.Dispose(); - - // ModSettings - GetAvailableModSettings.Dispose(); - GetCurrentModSettings.Dispose(); - TryInheritMod.Dispose(); - TrySetMod.Dispose(); - TrySetModPriority.Dispose(); - TrySetModSetting.Dispose(); - TrySetModSettings.Dispose(); - ModSettingChanged.Dispose(); - CopyModSettings.Dispose(); - - // Temporary - CreateTemporaryCollection.Dispose(); - RemoveTemporaryCollection.Dispose(); - CreateNamedTemporaryCollection.Dispose(); - RemoveTemporaryCollectionByName.Dispose(); - AssignTemporaryCollection.Dispose(); - AddTemporaryModAll.Dispose(); - AddTemporaryMod.Dispose(); - RemoveTemporaryModAll.Dispose(); - RemoveTemporaryMod.Dispose(); - - // Editing - ConvertTextureFile.Dispose(); - ConvertTextureData.Dispose(); - - // Resource Tree - GetGameObjectResourcePaths.Dispose(); - GetPlayerResourcePaths.Dispose(); - GetGameObjectResourcesOfType.Dispose(); - GetPlayerResourcesOfType.Dispose(); - GetGameObjectResourceTrees.Dispose(); - GetPlayerResourceTrees.Dispose(); - - Disposed.Invoke(); - Disposed.Dispose(); - } - - // Wrappers - private int DeprecatedVersion() - { - Penumbra.Log.Warning($"{Ipc.ApiVersion.Label} is outdated. Please use {Ipc.ApiVersions.Label} instead."); - return Api.ApiVersion.Breaking; - } - - private void OnClick(MouseButton click, object? item) - { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item); - ChangedItemClick.Invoke(click, type, id); - } - - private void OnTooltip(object? item) - { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(item); - ChangedItemTooltip.Invoke(type, id); - } - - private void EnabledChangeEvent(bool value) - => EnabledChange.Invoke(value); - - private void OnGameObjectRedrawn(IntPtr objectAddress, int objectTableIndex) - => GameObjectRedrawn.Invoke(objectAddress, objectTableIndex); - - private void CreatingCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr modelId, IntPtr customize, IntPtr equipData) - => CreatingCharacterBase.Invoke(gameObject, collectionName, modelId, customize, equipData); - - private void CreatedCharacterBaseEvent(IntPtr gameObject, string collectionName, IntPtr drawObject) - => CreatedCharacterBase.Invoke(gameObject, collectionName, drawObject); - - private void GameObjectResourceResolvedEvent(IntPtr gameObject, string gamePath, string localPath) - => GameObjectResourcePathResolved.Invoke(gameObject, gamePath, localPath); - - private void ModSettingChangedEvent(ModSettingChange type, string collection, string mod, bool inherited) - => ModSettingChanged.Invoke(type, collection, mod, inherited); - - private void ModDeletedEvent(string name) - => ModDeleted.Invoke(name); - - private void ModAddedEvent(string name) - => ModAdded.Invoke(name); - - private void ModMovedEvent(string from, string to) - => ModMoved.Invoke(from, to); -} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 72f0fb59..e1b32204 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -243,14 +243,14 @@ public sealed class CollectionCache : IDisposable continue; var config = settings.Settings[groupIndex]; - switch (group.Type) + switch (group) { - case GroupType.Single: - AddSubMod(group[config.AsIndex], mod); + case SingleModGroup single: + AddSubMod(single[config.AsIndex], mod); break; - case GroupType.Multi: + case MultiModGroup multi: { - foreach (var (option, _) in group.WithIndex() + foreach (var (option, _) in multi.WithIndex() .Where(p => config.HasFlag(p.Index)) .OrderByDescending(p => group.OptionPriority(p.Index))) AddSubMod(option, mod); diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index f6c6e14a..4b5c4337 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -119,7 +119,7 @@ public class CollectionCacheManager : IDisposable /// Does not create caches. /// public void CalculateEffectiveFileList(ModCollection collection) - => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, + => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier, () => CalculateEffectiveFileListInternal(collection)); private void CalculateEffectiveFileListInternal(ModCollection collection) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 3b865d4b..bc928360 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -116,7 +116,7 @@ public readonly struct ImcCache : IDisposable } private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path) - => new($"|{collection.Name}_{collection.ChangeCounter}|{path}"); + => new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}"); public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) => _imcFiles.TryGetValue(path, out file); diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 38679612..4e8ebe36 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -22,7 +22,7 @@ public class ActiveCollectionData public class ActiveCollections : ISavable, IDisposable { - public const int Version = 1; + public const int Version = 2; private readonly CollectionStorage _storage; private readonly CommunicatorService _communicator; @@ -261,16 +261,17 @@ public class ActiveCollections : ISavable, IDisposable var jObj = new JObject { { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, + { nameof(Default), Default.Id }, + { nameof(Interface), Interface.Id }, + { nameof(Current), Current.Id }, }; foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null) .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); + jObj.Add(type.ToString(), collection.Id); jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; jObj.WriteTo(j); } @@ -319,22 +320,16 @@ public class ActiveCollections : ISavable, IDisposable } } - /// - /// Load default, current, special, and character collections from config. - /// If a collection does not exist anymore, reset it to an appropriate default. - /// - private void LoadCollections() + private bool LoadCollectionsV1(JObject jObject) { - Penumbra.Log.Debug("[Collections] Reading collection assignments..."); - var configChanged = !Load(_saveService.FileNames, out var jObject); - - // Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed. - var defaultName = jObject[nameof(Default)]?.ToObject() - ?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name); + var configChanged = false; + // Load the default collection. If the name does not exist take the empty collection. + var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Name; if (!_storage.ByName(defaultName, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; } @@ -348,7 +343,8 @@ public class ActiveCollections : ISavable, IDisposable if (!_storage.ByName(interfaceName, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; } @@ -362,7 +358,8 @@ public class ActiveCollections : ISavable, IDisposable if (!_storage.ByName(currentName, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", NotificationType.Warning); + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", + NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; } @@ -393,11 +390,124 @@ public class ActiveCollections : ISavable, IDisposable Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments."); configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); - configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage); + configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 1); - // Save any changes. - if (configChanged) - _saveService.ImmediateSave(this); + return configChanged; + } + + private bool LoadCollectionsV2(JObject jObject) + { + var configChanged = false; + // Load the default collection. If the guid does not exist take the empty collection. + var defaultId = jObject[nameof(Default)]?.ToObject() ?? Guid.Empty; + if (!_storage.ById(defaultId, out var defaultCollection)) + { + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); + Default = ModCollection.Empty; + configChanged = true; + } + else + { + Default = defaultCollection; + } + + // Load the interface collection. If no string is set, use the name of whatever was set as Default. + var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Id; + if (!_storage.ById(interfaceId, out var interfaceCollection)) + { + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.", + NotificationType.Warning); + Interface = ModCollection.Empty; + configChanged = true; + } + else + { + Interface = interfaceCollection; + } + + // Load the current collection. + var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Id; + if (!_storage.ById(currentId, out var currentCollection)) + { + Penumbra.Messager.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.", + NotificationType.Warning); + Current = _storage.DefaultNamed; + configChanged = true; + } + else + { + Current = currentCollection; + } + + // Load special collections. + foreach (var (type, name, _) in CollectionTypeExtensions.Special) + { + var typeId = jObject[type.ToString()]?.ToObject(); + if (typeId == null) + continue; + + if (!_storage.ById(typeId.Value, out var typeCollection)) + { + Penumbra.Messager.NotificationMessage($"Last choice of {name} Collection {typeId.Value} is not available, removed.", + NotificationType.Warning); + configChanged = true; + } + else + { + SpecialCollections[(int)type] = typeCollection; + } + } + + Penumbra.Log.Debug("[Collections] Loaded non-individual collection assignments."); + + configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); + configChanged |= Individuals.ReadJObject(_saveService, this, jObject[nameof(Individuals)] as JArray, _storage, 2); + + return configChanged; + } + + private bool LoadCollectionsNew() + { + Current = _storage.DefaultNamed; + Default = _storage.DefaultNamed; + Interface = _storage.DefaultNamed; + return true; + } + + /// + /// Load default, current, special, and character collections from config. + /// If a collection does not exist anymore, reset it to an appropriate default. + /// + private void LoadCollections() + { + Penumbra.Log.Debug("[Collections] Reading collection assignments..."); + var configChanged = !Load(_saveService.FileNames, out var jObject); + var version = jObject["Version"]?.ToObject() ?? 0; + var changed = false; + switch (version) + { + case 1: + changed = LoadCollectionsV1(jObject); + break; + case 2: + changed = LoadCollectionsV2(jObject); + break; + case 0 when configChanged: + changed = LoadCollectionsNew(); + break; + case 0: + Penumbra.Messager.NotificationMessage("Active Collections File has unknown version and will be reset.", + NotificationType.Warning); + changed = LoadCollectionsNew(); + break; + } + + if (changed) + _saveService.ImmediateSaveSync(this); } /// @@ -410,7 +520,7 @@ public class ActiveCollections : ISavable, IDisposable var jObj = BackupService.GetJObjectForFile(fileNames, file); if (jObj == null) { - ret = new JObject(); + ret = []; return false; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index d0b61e57..2da2a569 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,7 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -48,6 +47,25 @@ public class CollectionStorage : IReadOnlyList, IDisposable return true; } + /// Find a collection by its id. If the GUID is empty, the empty collection is returned. + public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + { + if (id != Guid.Empty) + return _collections.FindFirst(c => c.Id == id, out collection); + + collection = ModCollection.Empty; + return true; + } + + /// Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. + public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection) + { + if (Guid.TryParse(identifier, out var guid)) + return ById(guid, out collection); + + return ByName(identifier, out collection); + } + public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) { _communicator = communicator; @@ -70,31 +88,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } - /// - /// Returns true if the name is not empty, it is not the name of the empty collection - /// and no existing collection results in the same filename as name. Also returns the fixed name. - /// - public bool CanAddCollection(string name, out string fixedName) - { - if (!IsValidName(name)) - { - fixedName = string.Empty; - return false; - } - - name = name.ToLowerInvariant(); - if (name.Length == 0 - || name == ModCollection.Empty.Name.ToLowerInvariant() - || _collections.Any(c => c.Name.ToLowerInvariant() == name)) - { - fixedName = string.Empty; - return false; - } - - fixedName = name; - return true; - } - /// /// Add a new collection of the given name. /// If duplicate is not-null, the new collection will be a duplicate of it. @@ -104,14 +97,6 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// public bool AddCollection(string name, ModCollection? duplicate) { - if (!CanAddCollection(name, out var fixedName)) - { - Penumbra.Messager.NotificationMessage( - $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, - false); - return false; - } - var newCollection = duplicate?.Duplicate(name, _collections.Count) ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); _collections.Add(newCollection); @@ -166,16 +151,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } - /// - /// Check if a name is valid to use for a collection. - /// Does not check for uniqueness. - /// - private static bool IsValidName(string name) - => name.Length is > 0 and < 64 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); - /// /// Read all collection files in the Collection Directory. - /// Ensure that the default named collection exists, and apply inheritances afterwards. + /// Ensure that the default named collection exists, and apply inheritances afterward. /// Duplicate collection files are not deleted, just not added here. /// private void ReadCollections(out ModCollection defaultNamedCollection) @@ -183,29 +161,46 @@ public class CollectionStorage : IReadOnlyList, IDisposable Penumbra.Log.Debug("[Collections] Reading saved collections..."); foreach (var file in _saveService.FileNames.CollectionFiles) { - if (!ModCollectionSave.LoadFromFile(file, out var name, out var version, out var settings, out var inheritance)) + if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance)) continue; - if (!IsValidName(name)) + if (id == Guid.Empty) { - // TODO: handle better. - Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", + Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning); + continue; + } + + if (ById(id, out _)) + { + Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.", NotificationType.Warning); continue; } - if (ByName(name, out _)) - { - Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", - NotificationType.Warning); - continue; - } - - var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); + var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) - Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", - NotificationType.Warning); + try + { + if (version >= 2) + { + File.Move(file.FullName, correctName, false); + Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", + NotificationType.Warning); + } + else + { + _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); + File.Delete(file.FullName); + Penumbra.Log.Information($"Migrated collection {name} to Guid {id}."); + } + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, + $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", NotificationType.Error); + } + _collections.Add(collection); } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 21a8cf8a..8a717b35 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -18,7 +18,7 @@ public partial class IndividualCollections foreach (var (name, identifiers, collection) in Assignments) { var tmp = identifiers[0].ToJson(); - tmp.Add("Collection", collection.Name); + tmp.Add("Collection", collection.Id); tmp.Add("Display", name); ret.Add(tmp); } @@ -26,18 +26,28 @@ public partial class IndividualCollections return ret; } - public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage) + public bool ReadJObject(SaveService saver, ActiveCollections parent, JArray? obj, CollectionStorage storage, int version) { if (_actors.Awaiter.IsCompletedSuccessfully) { - var ret = ReadJObjectInternal(obj, storage); + var ret = version switch + { + 1 => ReadJObjectInternalV1(obj, storage), + 2 => ReadJObjectInternalV2(obj, storage), + _ => true, + }; return ret; } Penumbra.Log.Debug("[Collections] Delayed reading individual assignments until actor service is ready..."); _actors.Awaiter.ContinueWith(_ => { - if (ReadJObjectInternal(obj, storage)) + if (version switch + { + 1 => ReadJObjectInternalV1(obj, storage), + 2 => ReadJObjectInternalV2(obj, storage), + _ => true, + }) saver.ImmediateSave(parent); IsLoaded = true; Loaded.Invoke(); @@ -45,7 +55,55 @@ public partial class IndividualCollections return false; } - private bool ReadJObjectInternal(JArray? obj, CollectionStorage storage) + private bool ReadJObjectInternalV1(JArray? obj, CollectionStorage storage) + { + Penumbra.Log.Debug("[Collections] Reading individual assignments..."); + if (obj == null) + { + Penumbra.Log.Debug($"[Collections] Finished reading {Count} individual assignments..."); + return true; + } + + foreach (var data in obj) + { + try + { + var identifier = _actors.FromJson(data as JObject); + var group = GetGroup(identifier); + if (group.Length == 0 || group.Any(i => !i.IsValid)) + { + Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.", + NotificationType.Error); + continue; + } + + var collectionName = data["Collection"]?.ToObject() ?? string.Empty; + if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) + { + Penumbra.Messager.NotificationMessage( + $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", + NotificationType.Warning); + continue; + } + + if (!Add(group, collection)) + { + Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + NotificationType.Warning); + } + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error); + } + } + + Penumbra.Log.Debug($"Finished reading {Count} individual assignments..."); + + return true; + } + + private bool ReadJObjectInternalV2(JArray? obj, CollectionStorage storage) { Penumbra.Log.Debug("[Collections] Reading individual assignments..."); if (obj == null) @@ -64,17 +122,17 @@ public partial class IndividualCollections if (group.Length == 0 || group.Any(i => !i.IsValid)) { changes = true; - Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed.", + Penumbra.Messager.NotificationMessage("Could not load an unknown individual collection, removed assignment.", NotificationType.Error); continue; } - var collectionName = data["Collection"]?.ToObject() ?? string.Empty; - if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) + var collectionId = data["Collection"]?.ToObject(); + if (!collectionId.HasValue || !storage.ById(collectionId.Value, out var collection)) { changes = true; Penumbra.Messager.NotificationMessage( - $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", + $"Could not load the collection {collectionId} as individual collection for {identifier}, removed assignment.", NotificationType.Warning); continue; } @@ -82,14 +140,14 @@ public partial class IndividualCollections if (!Add(group, collection)) { changes = true; - Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + Penumbra.Messager.NotificationMessage($"Could not add an individual collection for {identifier}, removed assignment.", NotificationType.Warning); } } catch (Exception e) { changes = true; - Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed.", NotificationType.Error); + Penumbra.Messager.NotificationMessage(e, $"Could not load an unknown individual collection, removed assignment.", NotificationType.Error); } } @@ -100,14 +158,6 @@ public partial class IndividualCollections internal void Migrate0To1(Dictionary old) { - static bool FindDataId(string name, NameDictionary data, out NpcId dataId) - { - var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), - new KeyValuePair(uint.MaxValue, string.Empty)); - dataId = kvp.Key; - return kvp.Value.Length > 0; - } - foreach (var (name, collection) in old) { var kind = ObjectKind.None; @@ -155,5 +205,15 @@ public partial class IndividualCollections NotificationType.Error); } } + + return; + + static bool FindDataId(string name, NameDictionary data, out NpcId dataId) + { + var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), + new KeyValuePair(uint.MaxValue, string.Empty)); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 771f9463..6003b5f9 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -138,7 +138,7 @@ public class InheritanceManager : IDisposable var changes = false; foreach (var subCollectionName in collection.InheritanceByName) { - if (_storage.ByName(subCollectionName, out var subCollection)) + if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection)) { if (AddInheritance(collection, subCollection, false)) continue; @@ -146,6 +146,15 @@ public class InheritanceManager : IDisposable changes = true; Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); } + else if (_storage.ByName(subCollectionName, out subCollection)) + { + changes = true; + Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID."); + if (AddInheritance(collection, subCollection, false)) + continue; + + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); + } else { Penumbra.Messager.NotificationMessage( diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 5d9de13d..de08c6a2 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,3 +1,4 @@ +using OtterGui; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; @@ -9,13 +10,13 @@ namespace Penumbra.Collections.Manager; public class TempCollectionManager : IDisposable { - public int GlobalChangeCounter { get; private set; } = 0; + public int GlobalChangeCounter { get; private set; } public readonly IndividualCollections Collections; - private readonly CommunicatorService _communicator; - private readonly CollectionStorage _storage; - private readonly ActorManager _actors; - private readonly Dictionary _customCollections = new(); + private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; + private readonly ActorManager _actors; + private readonly Dictionary _customCollections = []; public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorManager actors, CollectionStorage storage) { @@ -42,36 +43,36 @@ public class TempCollectionManager : IDisposable => _customCollections.Values; public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _customCollections.TryGetValue(name.ToLowerInvariant(), out collection); + => _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection); - public string CreateTemporaryCollection(string name) + public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + => _customCollections.TryGetValue(id, out collection); + + public Guid CreateTemporaryCollection(string name) { - if (_storage.ByName(name, out _)) - return string.Empty; - if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); - Penumbra.Log.Debug($"Creating temporary collection {collection.AnonymizedName}."); - if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection)) + Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}."); + if (_customCollections.TryAdd(collection.Id, collection)) { // Temporary collection created. _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty); - return collection.Name; + return collection.Id; } - return string.Empty; + return Guid.Empty; } - public bool RemoveTemporaryCollection(string collectionName) + public bool RemoveTemporaryCollection(Guid collectionId) { - if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection)) + if (!_customCollections.Remove(collectionId, out var collection)) { - Penumbra.Log.Debug($"Tried to delete temporary collection {collectionName.ToLowerInvariant()}, but did not exist."); + Penumbra.Log.Debug($"Tried to delete temporary collection {collectionId}, but did not exist."); return false; } - Penumbra.Log.Debug($"Deleted temporary collection {collection.AnonymizedName}."); + Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { @@ -80,7 +81,7 @@ public class TempCollectionManager : IDisposable // Temporary collection assignment removed. _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); - Penumbra.Log.Verbose($"Unassigned temporary collection {collection.AnonymizedName} from {Collections[i].DisplayName}."); + Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}."); Collections.Delete(i--); } @@ -98,32 +99,32 @@ public class TempCollectionManager : IDisposable return true; } - public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers) + public bool AddIdentifier(Guid collectionId, params ActorIdentifier[] identifiers) { - if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection)) + if (!_customCollections.TryGetValue(collectionId, out var collection)) return false; return AddIdentifier(collection, identifiers); } - public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue) + public bool AddIdentifier(Guid collectionId, string characterName, ushort worldId = ushort.MaxValue) { - if (!ByteString.FromString(characterName, out var byteString, false)) + if (!ByteString.FromString(characterName, out var byteString)) return false; var identifier = _actors.CreatePlayer(byteString, worldId); if (!identifier.IsValid) return false; - return AddIdentifier(collectionName, identifier); + return AddIdentifier(collectionId, identifier); } internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue) { - if (!ByteString.FromString(characterName, out var byteString, false)) + if (!ByteString.FromString(characterName, out var byteString)) return false; var identifier = _actors.CreatePlayer(byteString, worldId); - return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name); + return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index b63be6cd..c1143c71 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -17,7 +17,7 @@ namespace Penumbra.Collections; /// public partial class ModCollection { - public const int CurrentVersion = 1; + public const int CurrentVersion = 2; public const string DefaultCollectionName = "Default"; public const string EmptyCollectionName = "None"; @@ -27,15 +27,23 @@ public partial class ModCollection /// public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); - /// The name of a collection can not contain characters invalid in a path. - public string Name { get; internal init; } + /// The name of a collection. + public string Name { get; set; } = string.Empty; + + public Guid Id { get; } + + public string Identifier + => Id.ToString(); + + public string ShortIdentifier + => Identifier[..8]; public override string ToString() - => Name; + => Name.Length > 0 ? Name : ShortIdentifier; /// Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName - => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; + => this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier; /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } @@ -112,16 +120,16 @@ public partial class ModCollection public ModCollection Duplicate(string name, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), + return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. - public static ModCollection CreateFromData(SaveService saver, ModStorage mods, string name, int version, int index, + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index, Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(name, index, 0, version, new List(), new List(), allSettings) + var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings) { InheritanceByName = inheritances, }; @@ -134,8 +142,7 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(name, index, changeCounter, CurrentVersion, new List(), new List(), - new Dictionary()); + var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []); return ret; } @@ -143,9 +150,8 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), - new List(), - new Dictionary()); + return new ModCollection(Guid.Empty, name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], + []); } /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. @@ -193,10 +199,11 @@ public partial class ModCollection saver.ImmediateSave(new ModCollectionSave(mods, this)); } - private ModCollection(string name, int index, int changeCounter, int version, List appliedSettings, + private ModCollection(Guid id, string name, int index, int changeCounter, int version, List appliedSettings, List inheritsFrom, Dictionary settings) { Name = name; + Id = id; Index = index; ChangeCounter = changeCounter; Settings = appliedSettings; diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index f2cb4ada..acc38d83 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -28,6 +28,8 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteStartObject(); j.WritePropertyName("Version"); j.WriteValue(ModCollection.CurrentVersion); + j.WritePropertyName(nameof(ModCollection.Id)); + j.WriteValue(modCollection.Identifier); j.WritePropertyName(nameof(ModCollection.Name)); j.WriteValue(modCollection.Name); j.WritePropertyName(nameof(ModCollection.Settings)); @@ -55,20 +57,20 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Name)); + x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier)); j.WriteEndObject(); } - public static bool LoadFromFile(FileInfo file, out string name, out int version, out Dictionary settings, + public static bool LoadFromFile(FileInfo file, out Guid id, out string name, out int version, out Dictionary settings, out IReadOnlyList inheritance) { - settings = new Dictionary(); - inheritance = Array.Empty(); + settings = []; + inheritance = []; if (!file.Exists) { Penumbra.Log.Error("Could not read collection because file does not exist."); - name = string.Empty; - + name = string.Empty; + id = Guid.Empty; version = 0; return false; } @@ -76,8 +78,9 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection try { var obj = JObject.Parse(File.ReadAllText(file.FullName)); - name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; version = obj["Version"]?.ToObject() ?? 0; + name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; + id = obj[nameof(ModCollection.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; @@ -87,6 +90,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection { name = string.Empty; version = 0; + id = Guid.Empty; Penumbra.Log.Error($"Could not read collection information from file:\n{e}"); return false; } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 537b08da..4e1d6453 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -513,7 +513,7 @@ public class CommandHandler : IDisposable collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager.Storage.ByName(lowerName, out var c) + : _collectionManager.Storage.ByIdentifier(lowerName, out var c) ? c : null; if (collection != null) diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 754570e2..554e2221 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; using Penumbra.Api.Enums; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ public sealed class ChangedItemClick() : EventWrapper + /// Default = 0, /// diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 10607da4..2dcced35 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class ChangedItemHover() : EventWrapper + /// Default = 0, /// diff --git a/Penumbra/Communication/CreatedCharacterBase.cs b/Penumbra/Communication/CreatedCharacterBase.cs index 397f7bfd..8992f9fc 100644 --- a/Penumbra/Communication/CreatedCharacterBase.cs +++ b/Penumbra/Communication/CreatedCharacterBase.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Collections; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 2f249c14..8a906ca0 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Services; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ namespace Penumbra.Communication; /// Parameter is a pointer to the equip data array. /// public sealed class CreatingCharacterBase() - : EventWrapper(nameof(CreatingCharacterBase)) + : EventWrapper(nameof(CreatingCharacterBase)) { public enum Priority { diff --git a/Penumbra/Communication/EnabledChanged.cs b/Penumbra/Communication/EnabledChanged.cs index be6343b7..846b1a58 100644 --- a/Penumbra/Communication/EnabledChanged.cs +++ b/Penumbra/Communication/EnabledChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.IpcSubscribers; namespace Penumbra.Communication; @@ -13,7 +14,7 @@ public sealed class EnabledChanged() : EventWrapper + /// Api = int.MinValue, /// diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 20d13b20..02293873 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; namespace Penumbra.Communication; diff --git a/Penumbra/Communication/ModFileChanged.cs b/Penumbra/Communication/ModFileChanged.cs index 8b4b6f5d..8cda48e9 100644 --- a/Penumbra/Communication/ModFileChanged.cs +++ b/Penumbra/Communication/ModFileChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Editor; diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index f02b17dc..0df58b5f 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 01c8fa64..1e4f8d36 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -19,8 +20,11 @@ public sealed class ModPathChanged() { public enum Priority { - /// - Api = int.MinValue, + /// + ApiMods = int.MinValue, + + /// + ApiModSettings = int.MinValue, /// EphemeralConfig = -500, diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 412b3003..968f78a7 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; diff --git a/Penumbra/Communication/PostEnabledDraw.cs b/Penumbra/Communication/PostEnabledDraw.cs index 68637442..e21f0183 100644 --- a/Penumbra/Communication/PostEnabledDraw.cs +++ b/Penumbra/Communication/PostEnabledDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class PostEnabledDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Communication/PostSettingsPanelDraw.cs b/Penumbra/Communication/PostSettingsPanelDraw.cs index a918b610..525ac73e 100644 --- a/Penumbra/Communication/PostSettingsPanelDraw.cs +++ b/Penumbra/Communication/PostSettingsPanelDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class PostSettingsPanelDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Communication/PreSettingsPanelDraw.cs b/Penumbra/Communication/PreSettingsPanelDraw.cs index cda00d78..33f6b4e1 100644 --- a/Penumbra/Communication/PreSettingsPanelDraw.cs +++ b/Penumbra/Communication/PreSettingsPanelDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class PreSettingsPanelDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Communication/PreSettingsTabBarDraw.cs b/Penumbra/Communication/PreSettingsTabBarDraw.cs index 2c14cdf1..8614bbbe 100644 --- a/Penumbra/Communication/PreSettingsTabBarDraw.cs +++ b/Penumbra/Communication/PreSettingsTabBarDraw.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -14,7 +15,7 @@ public sealed class PreSettingsTabBarDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/GuidExtensions.cs b/Penumbra/GuidExtensions.cs new file mode 100644 index 00000000..fcbc8a3b --- /dev/null +++ b/Penumbra/GuidExtensions.cs @@ -0,0 +1,254 @@ +using System.Collections.Frozen; +using OtterGui; + +namespace Penumbra; + +public static class GuidExtensions +{ + private const string Chars = + "0123456789" + + "abcdefghij" + + "klmnopqrst" + + "uv"; + + private static ReadOnlySpan Bytes + => "0123456789abcdefghijklmnopqrstuv"u8; + + private static readonly FrozenDictionary + ReverseChars = Chars.WithIndex().ToFrozenDictionary(t => t.Value, t => (byte)t.Index); + + private static readonly FrozenDictionary ReverseBytes = + ReverseChars.ToFrozenDictionary(kvp => (byte)kvp.Key, kvp => kvp.Value); + + public static unsafe string OptimizedString(this Guid guid) + { + var bytes = stackalloc ulong[2]; + if (!guid.TryWriteBytes(new Span(bytes, 16))) + return guid.ToString("N"); + + var u1 = bytes[0]; + var u2 = bytes[1]; + Span text = + [ + Chars[(int)(u1 & 0x1F)], + Chars[(int)((u1 >> 5) & 0x1F)], + Chars[(int)((u1 >> 10) & 0x1F)], + Chars[(int)((u1 >> 15) & 0x1F)], + Chars[(int)((u1 >> 20) & 0x1F)], + Chars[(int)((u1 >> 25) & 0x1F)], + Chars[(int)((u1 >> 30) & 0x1F)], + Chars[(int)((u1 >> 35) & 0x1F)], + Chars[(int)((u1 >> 40) & 0x1F)], + Chars[(int)((u1 >> 45) & 0x1F)], + Chars[(int)((u1 >> 50) & 0x1F)], + Chars[(int)((u1 >> 55) & 0x1F)], + Chars[(int)((u1 >> 60) | ((u2 & 0x01) << 4))], + Chars[(int)((u2 >> 1) & 0x1F)], + Chars[(int)((u2 >> 6) & 0x1F)], + Chars[(int)((u2 >> 11) & 0x1F)], + Chars[(int)((u2 >> 16) & 0x1F)], + Chars[(int)((u2 >> 21) & 0x1F)], + Chars[(int)((u2 >> 26) & 0x1F)], + Chars[(int)((u2 >> 31) & 0x1F)], + Chars[(int)((u2 >> 36) & 0x1F)], + Chars[(int)((u2 >> 41) & 0x1F)], + Chars[(int)((u2 >> 46) & 0x1F)], + Chars[(int)((u2 >> 51) & 0x1F)], + Chars[(int)((u2 >> 56) & 0x1F)], + Chars[(int)((u2 >> 61) & 0x1F)], + ]; + return new string(text); + } + + public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) + { + if (text.Length != 26) + return Return(out guid); + + var bytes = stackalloc ulong[2]; + if (!ReverseChars.TryGetValue(text[0], out var b0)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[1], out var b1)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[2], out var b2)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[3], out var b3)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[4], out var b4)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[5], out var b5)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[6], out var b6)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[7], out var b7)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[8], out var b8)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[9], out var b9)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[10], out var b10)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[11], out var b11)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[12], out var b12)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[13], out var b13)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[14], out var b14)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[15], out var b15)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[16], out var b16)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[17], out var b17)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[18], out var b18)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[19], out var b19)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[20], out var b20)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[21], out var b21)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[22], out var b22)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[23], out var b23)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[24], out var b24)) + return Return(out guid); + if (!ReverseChars.TryGetValue(text[25], out var b25)) + return Return(out guid); + + bytes[0] = b0 + | ((ulong)b1 << 5) + | ((ulong)b2 << 10) + | ((ulong)b3 << 15) + | ((ulong)b4 << 20) + | ((ulong)b5 << 25) + | ((ulong)b6 << 30) + | ((ulong)b7 << 35) + | ((ulong)b8 << 40) + | ((ulong)b9 << 45) + | ((ulong)b10 << 50) + | ((ulong)b11 << 55) + | ((ulong)b12 << 60); + bytes[1] = ((ulong)b12 >> 4) + | ((ulong)b13 << 1) + | ((ulong)b14 << 6) + | ((ulong)b15 << 11) + | ((ulong)b16 << 16) + | ((ulong)b17 << 21) + | ((ulong)b18 << 26) + | ((ulong)b19 << 31) + | ((ulong)b20 << 36) + | ((ulong)b21 << 41) + | ((ulong)b22 << 46) + | ((ulong)b23 << 51) + | ((ulong)b24 << 56) + | ((ulong)b25 << 61); + guid = new Guid(new Span(bytes, 16)); + return true; + + static bool Return(out Guid guid) + { + guid = Guid.Empty; + return false; + } + } + + public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) + { + if (text.Length != 26) + return Return(out guid); + + var bytes = stackalloc ulong[2]; + if (!ReverseBytes.TryGetValue(text[0], out var b0)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[1], out var b1)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[2], out var b2)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[3], out var b3)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[4], out var b4)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[5], out var b5)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[6], out var b6)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[7], out var b7)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[8], out var b8)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[9], out var b9)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[10], out var b10)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[11], out var b11)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[12], out var b12)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[13], out var b13)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[14], out var b14)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[15], out var b15)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[16], out var b16)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[17], out var b17)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[18], out var b18)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[19], out var b19)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[20], out var b20)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[21], out var b21)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[22], out var b22)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[23], out var b23)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[24], out var b24)) + return Return(out guid); + if (!ReverseBytes.TryGetValue(text[25], out var b25)) + return Return(out guid); + + bytes[0] = b0 + | ((ulong)b1 << 5) + | ((ulong)b2 << 10) + | ((ulong)b3 << 15) + | ((ulong)b4 << 20) + | ((ulong)b5 << 25) + | ((ulong)b6 << 30) + | ((ulong)b7 << 35) + | ((ulong)b8 << 40) + | ((ulong)b9 << 45) + | ((ulong)b10 << 50) + | ((ulong)b11 << 55) + | ((ulong)b12 << 60); + bytes[1] = ((ulong)b12 >> 4) + | ((ulong)b13 << 1) + | ((ulong)b14 << 6) + | ((ulong)b15 << 11) + | ((ulong)b16 << 16) + | ((ulong)b17 << 21) + | ((ulong)b18 << 26) + | ((ulong)b19 << 31) + | ((ulong)b20 << 36) + | ((ulong)b21 << 41) + | ((ulong)b22 << 46) + | ((ulong)b23 << 51) + | ((ulong)b24 << 56) + | ((ulong)b25 << 61); + guid = new Guid(new Span(bytes, 16)); + return true; + + static bool Return(out Guid guid) + { + guid = Guid.Empty; + return false; + } + } +} diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index a3400540..5f07ffc5 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -130,7 +130,7 @@ public sealed unsafe class MetaState : IDisposable _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Name, (nint)modelCharaId, (nint)customize, (nint)equipData); + _lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 7c16b97b..5c3d8d19 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,3 +1,4 @@ +using System.Runtime; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -43,8 +44,8 @@ public class PathResolver : IDisposable } /// Obtain a temporary or permanent collection by name. - public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionByName(name, out collection) || _collectionManager.Storage.ByName(name, out collection); + public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + => _tempCollections.CollectionById(id, out collection) || _collectionManager.Storage.ById(id, out collection); /// Try to resolve the given game path to the replaced path. public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) @@ -136,9 +137,10 @@ public class PathResolver : IDisposable return; var lastUnderscore = additionalData.LastIndexOf((byte)'_'); - var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString(); + var idString = lastUnderscore == -1 ? additionalData : additionalData.Substring(0, lastUnderscore); if (Utf8GamePath.FromByteString(path, out var gamePath) - && CollectionByName(name, out var collection) + && GuidExtensions.FromOptimizedString(idString.Span, out var id) + && CollectionById(id, out var collection) && collection.HasCache && collection.GetImcFile(gamePath, out var file)) { diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 2359c36e..844baaa9 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -76,7 +76,7 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> GetResourcePathDictionaries(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary>> GetResourcePathDictionaries( + IEnumerable<(Character, ResourceTree)> resourceTrees) { var pathDictionaries = new Dictionary>>(4); @@ -23,8 +25,7 @@ internal static class ResourceTreeApiHelper CollectResourcePaths(pathDictionary, resourceTree); } - return pathDictionaries.ToDictionary(pair => pair.Key, - pair => (IReadOnlyDictionary)pair.Value.ToDictionary(pair => pair.Key, pair => pair.Value.ToArray()).AsReadOnly()); + return pathDictionaries; } private static void CollectResourcePaths(Dictionary> pathDictionary, ResourceTree resourceTree) @@ -37,7 +38,7 @@ internal static class ResourceTreeApiHelper var fullPath = node.FullPath.ToPath(); if (!pathDictionary.TryGetValue(fullPath, out var gamePaths)) { - gamePaths = new(); + gamePaths = []; pathDictionary.Add(fullPath, gamePaths); } @@ -46,17 +47,17 @@ internal static class ResourceTreeApiHelper } } - public static Dictionary> GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, + public static Dictionary GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, ResourceType type) { - var resDictionaries = new Dictionary>(4); + var resDictionaries = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) { if (resDictionaries.ContainsKey(gameObject.ObjectIndex)) continue; - var resDictionary = new Dictionary(); - resDictionaries.Add(gameObject.ObjectIndex, resDictionary); + var resDictionary = new Dictionary(); + resDictionaries.Add(gameObject.ObjectIndex, new GameResourceDict(resDictionary)); foreach (var node in resourceTree.FlatNodes) { @@ -66,38 +67,16 @@ internal static class ResourceTreeApiHelper continue; var fullPath = node.FullPath.ToPath(); - resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, ChangedItemDrawer.ToApiIcon(node.Icon))); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon))); } } - return resDictionaries.ToDictionary(pair => pair.Key, - pair => (IReadOnlyDictionary)pair.Value.AsReadOnly()); + return resDictionaries; } - public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) { - static Ipc.ResourceNode GetIpcNode(ResourceNode node) => - new() - { - Type = node.Type, - Icon = ChangedItemDrawer.ToApiIcon(node.Icon), - Name = node.Name, - GamePath = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), - ActualPath = node.FullPath.ToString(), - ObjectAddress = node.ObjectAddress, - ResourceHandle = node.ResourceHandle, - Children = node.Children.Select(GetIpcNode).ToList(), - }; - - static Ipc.ResourceTree GetIpcTree(ResourceTree tree) => - new() - { - Name = tree.Name, - RaceCode = (ushort)tree.RaceCode, - Nodes = tree.Nodes.Select(GetIpcNode).ToList(), - }; - - var resDictionary = new Dictionary(4); + var resDictionary = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) { if (resDictionary.ContainsKey(gameObject.ObjectIndex)) @@ -107,5 +86,38 @@ internal static class ResourceTreeApiHelper } return resDictionary; + + static JObject GetIpcTree(ResourceTree tree) + { + var ret = new JObject + { + [nameof(ResourceTreeDto.Name)] = tree.Name, + [nameof(ResourceTreeDto.RaceCode)] = (ushort)tree.RaceCode, + }; + var children = new JArray(); + foreach (var child in tree.Nodes) + children.Add(GetIpcNode(child)); + ret[nameof(ResourceTreeDto.Nodes)] = children; + return ret; + } + + static JObject GetIpcNode(ResourceNode node) + { + var ret = new JObject + { + [nameof(ResourceNodeDto.Type)] = new JValue(node.Type), + [nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)), + [nameof(ResourceNodeDto.Name)] = node.Name, + [nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), + [nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(), + [nameof(ResourceNodeDto.ObjectAddress)] = node.ObjectAddress, + [nameof(ResourceNodeDto.ResourceHandle)] = node.ResourceHandle, + }; + var children = new JArray(); + foreach (var child in node.Children) + children.Add(GetIpcNode(child)); + ret[nameof(ResourceNodeDto.Children)] = children; + return ret; + } } } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 0ffdc4af..9efb8a3f 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -272,22 +272,19 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return; var group = mod.Groups[groupIdx]; - if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); - return; - } - - o.SetPosition(groupIdx, group.Count); - switch (group) { + case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }: + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); + return; case SingleModGroup s: + o.SetPosition(groupIdx, s.Count); s.OptionData.Add(o); break; case MultiModGroup m: + o.SetPosition(groupIdx, m.Count); m.PrioritizedOptions.Add((o, priority)); break; } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 1e5df6b9..65b8ddd9 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -19,7 +19,7 @@ public class ModCombo : FilterComboCache public class ModStorage : IReadOnlyList { /// The actual list of mods. - protected readonly List Mods = new(); + protected readonly List Mods = []; public int Count => Mods.Count; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index e9e2a93b..2daf31e6 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -4,9 +4,9 @@ using Penumbra.Services; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IEnumerable +public interface IModGroup : IReadOnlyCollection { - public const int MaxMultiOptions = 32; + public const int MaxMultiOptions = 63; public string Name { get; } public string Description { get; } @@ -18,15 +18,7 @@ public interface IModGroup : IEnumerable public ISubMod this[Index idx] { get; } - public int Count { get; } - - public bool IsOption - => Type switch - { - GroupType.Single => Count > 1, - GroupType.Multi => Count > 0, - _ => false, - }; + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); @@ -94,11 +86,13 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName("Options"); j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) + { ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch { GroupType.Multi => _group.OptionPriority(idx), - _ => null, + _ => null, }); + } j.WriteEndArray(); j.WriteEndObject(); diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index b79b3242..380b242c 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -12,7 +12,7 @@ public class ModSettings { public static readonly ModSettings Empty = new(); public SettingList Settings { get; private init; } = []; - public ModPriority Priority { get; set; } + public ModPriority Priority { get; set; } public bool Enabled { get; set; } // Create an independent copy of the current settings. @@ -152,7 +152,7 @@ public class ModSettings public struct SavedSettings { public Dictionary Settings; - public ModPriority Priority; + public ModPriority Priority; public bool Enabled; public SavedSettings DeepCopy() @@ -203,9 +203,9 @@ public class ModSettings // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. // Does not repair settings but ignores settings not fitting to the given mod. - public (bool Enabled, ModPriority Priority, Dictionary> Settings) ConvertToShareable(Mod mod) + public (bool Enabled, ModPriority Priority, Dictionary> Settings) ConvertToShareable(Mod mod) { - var dict = new Dictionary>(Settings.Count); + var dict = new Dictionary>(Settings.Count); foreach (var (setting, idx) in Settings.WithIndex()) { if (idx >= mod.Groups.Count) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 444e8e2c..7479cd54 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -25,6 +25,9 @@ public sealed class MultiModGroup : IModGroup public ISubMod this[Index idx] => PrioritizedOptions[idx].Mod; + public bool IsOption + => Count > 0; + [JsonIgnore] public int Count => PrioritizedOptions.Count; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 0bfa04f4..74769c7e 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -25,6 +25,9 @@ public sealed class SingleModGroup : IModGroup public ISubMod this[Index idx] => OptionData[idx]; + public bool IsOption + => Count > 1; + [JsonIgnore] public int Count => OptionData.Count; diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 4de2ac13..6be07881 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -53,7 +53,7 @@ public class TemporaryMod : IMod dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null); + $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", null, null); var mod = new Mod(dir); var defaultMod = mod.Default; foreach (var (gamePath, fullPath) in collection.ResolvedFiles) @@ -86,11 +86,11 @@ public class TemporaryMod : IMod saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); - Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}."); + Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } catch (Exception e) { - Penumbra.Log.Error($"Could not save temporary collection {collection.Name} to permanent Mod:\n{e}"); + Penumbra.Log.Error($"Could not save temporary collection {collection.Identifier} to permanent Mod:\n{e}"); if (dir != null && Directory.Exists(dir.FullName)) { try diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b76780c0..42be0aa3 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -20,6 +20,7 @@ using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; using Penumbra.GameData.Enums; using Penumbra.UI; +using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra; @@ -105,8 +106,7 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { - var api = _services.GetService(); - _services.GetService(); + _services.GetService(); _communicatorService.ChangedItemHover.Subscribe(it => { if (it is (Item, FullEquipType)) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index b07917e8..c8961579 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + net8.0-windows preview diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index d1e952f1..e775d81a 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -223,7 +223,7 @@ public class ConfigMigrationService(SaveService saveService) : IService try { var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); - if (jObject[nameof(ModCollection.Name)]?.ToObject() == ForcedCollection) + if (jObject["Name"]?.ToObject() == ForcedCollection) continue; jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); @@ -365,7 +365,7 @@ public class ConfigMigrationService(SaveService saveService) : IService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, []); + var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, 0, 1, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 078b812b..6805e7db 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -239,7 +239,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(character); lock (_eventWriter) { - _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type); + _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Id, type); } } catch (Exception ex) @@ -248,7 +248,7 @@ public sealed class CrashHandlerService : IDisposable, IService } } - private void OnCreatingCharacterBase(nint address, string collection, nint _1, nint _2, nint _3) + private void OnCreatingCharacterBase(nint address, Guid collection, nint _1, nint _2, nint _3) { if (_eventWriter == null) return; @@ -293,7 +293,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(resolveData.AssociatedGameObject); lock (_eventWriter) { - _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name, + _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Id, manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 40c63f15..e1c482f7 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -24,7 +24,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) - => CollectionFile(collection.Name); + => CollectionFile(collection.Identifier); /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 9e6071b4..e758aa35 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; using Penumbra.Api; +using Penumbra.Api.Api; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; @@ -30,8 +31,10 @@ using Penumbra.UI.ModsTab; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; using Penumbra.UI.Tabs.Debug; +using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using Penumbra.Api.IpcTester; namespace Penumbra.Services; @@ -195,10 +198,5 @@ public static class StaticServiceManager .AddSingleton(); private static ServiceManager AddApi(this ServiceManager services) - => services.AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + => services.AddSingleton(x => x.GetRequiredService()); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 9a38a5d5..10956deb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -25,8 +25,8 @@ public partial class ModEditWindow var resources = ResourceTreeApiHelper .GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) .Values - .SelectMany(resources => resources.Values) - .Select(resource => resource.Item1); + .SelectMany(r => r.Values) + .Select(r => r.Item1); return new HashSet(resources, StringComparer.OrdinalIgnoreCase); } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 4d922af5..cbeabbd6 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -2,11 +2,13 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -28,8 +30,8 @@ public sealed class CollectionPanel : IDisposable private readonly IndividualAssignmentUi _individualAssignmentUi; private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; - - private readonly IFontHandle _nameFont; + private readonly FilenameService _fileNames; + private readonly IFontHandle _nameFont; private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); @@ -38,7 +40,7 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods) + CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames) { _collections = manager.Storage; _active = manager.Active; @@ -46,6 +48,7 @@ public sealed class CollectionPanel : IDisposable _actors = actors; _targets = targets; _mods = mods; + _fileNames = fileNames; _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); _inheritanceUi = new InheritanceUi(manager, _selector); _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); @@ -206,12 +209,57 @@ public sealed class CollectionPanel : IDisposable var collection = _active.Current; DrawCollectionName(collection); DrawStatistics(collection); + DrawCollectionData(collection); _inheritanceUi.Draw(); ImGui.Separator(); DrawInactiveSettingsList(collection); DrawSettingsList(collection); } + private void DrawCollectionData(ModCollection collection) + { + ImGui.Dummy(Vector2.Zero); + ImGui.BeginGroup(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Name"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Identifier"); + ImGui.EndGroup(); + ImGui.SameLine(); + ImGui.BeginGroup(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var name = collection.Name; + var identifier = collection.Identifier; + var width = ImGui.GetContentRegionAvail().X; + var fileName = _fileNames.CollectionFile(collection); + ImGui.SetNextItemWidth(width); + ImGui.InputText("##name", ref name, 128); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Could not open file {fileName}.", $"Could not open file {fileName}", + NotificationType.Warning); + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(identifier); + + ImGuiUtil.HoverTooltip( + $"Open the file\n\t{fileName}\ncontaining this design in the .json-editor of your choice.\n\nRight-Click to copy identifier to clipboard."); + + ImGui.EndGroup(); + ImGui.Dummy(Vector2.Zero); + ImGui.Separator(); + ImGui.Dummy(Vector2.Zero); + } + private void DrawContext(bool open, ModCollection? collection, CollectionType type, ActorIdentifier identifier, string text, char suffix) { var label = $"{type}{text}{suffix}"; diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index e568ecaf..fac85d4d 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -24,7 +24,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, TutorialService tutorial) - : base(new List(), Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) + : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) { _config = config; _communicator = communicator; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 3c6a3ed9..fe1471b3 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -41,12 +41,12 @@ public sealed class CollectionsTab : IDisposable, ITab } public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, - CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial) + CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames) { _config = configuration.Ephemeral; _tutorial = tutorial; _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames); } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 78014054..4649e548 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -55,7 +55,7 @@ public static class CrashDataExtensions ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn(character.ThreadId.ToString()); ImGuiUtil.DrawTableColumn(character.CharacterName); - ImGuiUtil.DrawTableColumn(character.CollectionName); + ImGuiUtil.DrawTableColumn(character.CollectionId.ToString()); ImGuiUtil.DrawTableColumn(character.CharacterAddress); ImGuiUtil.DrawTableColumn(character.Timestamp.ToString()); }, ImGui.GetTextLineHeightWithSpacing()); @@ -79,7 +79,7 @@ public static class CrashDataExtensions ImGuiUtil.DrawTableColumn(file.ActualFileName); ImGuiUtil.DrawTableColumn(file.RequestedFileName); ImGuiUtil.DrawTableColumn(file.CharacterName); - ImGuiUtil.DrawTableColumn(file.CollectionName); + ImGuiUtil.DrawTableColumn(file.CollectionId.ToString()); ImGuiUtil.DrawTableColumn(file.CharacterAddress); ImGuiUtil.DrawTableColumn(file.Timestamp.ToString()); }, ImGui.GetTextLineHeightWithSpacing()); @@ -102,7 +102,7 @@ public static class CrashDataExtensions ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString()); ImGuiUtil.DrawTableColumn(vfx.InvocationType); ImGuiUtil.DrawTableColumn(vfx.CharacterName); - ImGuiUtil.DrawTableColumn(vfx.CollectionName); + ImGuiUtil.DrawTableColumn(vfx.CollectionId.ToString()); ImGuiUtil.DrawTableColumn(vfx.CharacterAddress); ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString()); }, ImGui.GetTextLineHeightWithSpacing()); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a956d2d..1813a7e3 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -40,6 +40,7 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; +using Penumbra.Api.IpcTester; namespace Penumbra.UI.Tabs.Debug; @@ -76,7 +77,6 @@ public class DebugTab : Window, ITab private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly ResourceManagerService _resourceManager; - private readonly PenumbraIpcProviders _ipc; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly PathState _pathState; @@ -100,7 +100,7 @@ public class DebugTab : Window, ITab IClientState clientState, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, - ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, + ResourceManagerService resourceManager, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, @@ -124,7 +124,6 @@ public class DebugTab : Window, ITab _characterUtility = characterUtility; _residentResources = residentResources; _resourceManager = resourceManager; - _ipc = ipc; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _pathState = pathState; @@ -440,7 +439,9 @@ public class DebugTab : Window, ITab : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind ==(byte) ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.AsObject->DataID}" : identifier.DataId.ToString(); + var id = obj.AsObject->ObjectKind == (byte)ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->DataID}" + : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); } @@ -969,13 +970,8 @@ public class DebugTab : Window, ITab /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { - if (!ImGui.CollapsingHeader("IPC")) - { - _ipcTester.UnsubscribeEvents(); - return; - } - - _ipcTester.Draw(); + if (ImGui.CollapsingHeader("IPC")) + _ipcTester.Draw(); } /// Helper to print a property and its value in a 2-column table. From 1ef9346eab9e827bf31664dae0171656d2cae1af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 12:42:16 +0200 Subject: [PATCH 026/865] Allow renaming of collection. --- .../Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Collections/ModCollection.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 37 ++++++++++++------- Penumbra/UI/CollectionTab/InheritanceUi.cs | 29 +++++---------- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 7c29676d..b073e731 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -77,7 +77,7 @@ public partial class ModCollection else { _cache.Meta.SetFiles(); - Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}."); + Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Identifier}."); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index c1143c71..327d6544 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -28,7 +28,7 @@ public partial class ModCollection public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); /// The name of a collection. - public string Name { get; set; } = string.Empty; + public string Name { get; set; } public Guid Id { get; } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cbeabbd6..8625335e 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -35,7 +35,8 @@ public sealed class CollectionPanel : IDisposable private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); - private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = new(); + private readonly List<(CollectionType Type, ActorIdentifier Identifier)> _inUseCache = []; + private string? _newName; private int _draggedIndividualAssignment = -1; @@ -93,6 +94,18 @@ public sealed class CollectionPanel : IDisposable var first = true; + Button(CollectionType.NonPlayerChild); + Button(CollectionType.NonPlayerElderly); + foreach (var race in Enum.GetValues().Skip(1)) + { + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); + Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); + } + + return; + void Button(CollectionType type) { var (name, border) = Buttons[type]; @@ -112,16 +125,6 @@ public sealed class CollectionPanel : IDisposable if (ImGui.GetContentRegionAvail().X < buttonWidth.X + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X) ImGui.NewLine(); } - - Button(CollectionType.NonPlayerChild); - Button(CollectionType.NonPlayerElderly); - foreach (var race in Enum.GetValues().Skip(1)) - { - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, false)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Male, true)); - Button(CollectionTypeExtensions.FromParts(race, Gender.Female, true)); - } } /// Draw the panel containing new and existing individual assignments. @@ -228,12 +231,20 @@ public sealed class CollectionPanel : IDisposable ImGui.SameLine(); ImGui.BeginGroup(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var name = collection.Name; + var name = _newName ?? collection.Name; var identifier = collection.Identifier; var width = ImGui.GetContentRegionAvail().X; var fileName = _fileNames.CollectionFile(collection); ImGui.SetNextItemWidth(width); - ImGui.InputText("##name", ref name, 128); + if (ImGui.InputText("##name", ref name, 128)) + _newName = name; + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null) + { + collection.Name = _newName; + _newName = null; + } + else if (ImGui.IsItemDeactivated()) + _newName = null; using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 88344e6a..2290592d 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -2,30 +2,21 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public class InheritanceUi +public class InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) : IUiService { private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly CollectionStorage _collections; - private readonly ActiveCollections _active; - private readonly InheritanceManager _inheritance; - private readonly CollectionSelector _selector; - - public InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) - { - _selector = selector; - _collections = collectionManager.Storage; - _active = collectionManager.Active; - _inheritance = collectionManager.Inheritances; - } - + private readonly CollectionStorage _collections = collectionManager.Storage; + private readonly ActiveCollections _active = collectionManager.Active; + private readonly InheritanceManager _inheritance = collectionManager.Inheritances; /// Draw the whole inheritance block. public void Draw() @@ -59,7 +50,7 @@ public class InheritanceUi private (int, int)? _inheritanceAction; private ModCollection? _newCurrentCollection; - private void DrawRightText() + private static void DrawRightText() { using var group = ImRaii.Group(); ImGuiUtil.TextWrapped( @@ -68,7 +59,7 @@ public class InheritanceUi "You can select inheritances from the combo below to add them.\nSince the order of inheritances is important, you can reorder them here via drag and drop.\nYou can also delete inheritances by dragging them onto the trash can."); } - private void DrawHelpPopup() + private static void DrawHelpPopup() => ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 20 * ImGui.GetTextLineHeightWithSpacing()), () => { ImGui.NewLine(); @@ -123,7 +114,7 @@ public class InheritanceUi _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); - ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Name}", + ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Id}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); DrawInheritanceTreeClicks(inheritance, false); @@ -134,7 +125,7 @@ public class InheritanceUi // Draw the notch and increase the line length. var midPoint = (minRect.Y + maxRect.Y) / 2f - 1f; - drawList.AddLine(new Vector2(lineStart.X, midPoint), new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText, + drawList.AddLine(lineStart with { Y = midPoint }, new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText, UiHelpers.Scale); lineEnd.Y = midPoint; } @@ -321,5 +312,5 @@ public class InheritanceUi } private string Name(ModCollection collection) - => _selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + => selector.IncognitoMode ? collection.AnonymizedName : collection.Name; } From 791583e183d4afd0a72e047d988f8ad5cb62a728 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 12:51:33 +0200 Subject: [PATCH 027/865] Silence readme warnings. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index e5c8f544..9bbc3b98 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit e5c8f5446879e2e0e541eb4d8fee15e98b1885bc +Subproject commit 9bbc3b98efc2af3707adc75b716d4f3072908e31 diff --git a/Penumbra.String b/Penumbra.String index 14e00f77..caa58c5c 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 14e00f77d42bc677e02325660db765ef11932560 +Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488 From e4f9150c9fc22ab85582f13a5dc866b115e61626 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:30:47 +0200 Subject: [PATCH 028/865] Fix? --- Penumbra.Api | 2 +- Penumbra/Api/Api/PluginStateApi.cs | 8 ++++---- Penumbra/Communication/ModDirectoryChanged.cs | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 9bbc3b98..cd56068a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 9bbc3b98efc2af3707adc75b716d4f3072908e31 +Subproject commit cd56068aac3762c7b011d13a04637a3c3f09775f diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index 2e87486f..e1eec1b2 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -13,16 +13,16 @@ public class PluginStateApi(Configuration config, CommunicatorService communicat public string GetConfiguration() => JsonConvert.SerializeObject(config, Formatting.Indented); - public event Action? ModDirectoryChanged + public event Action ModDirectoryChanged { - add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); - remove => communicator.ModDirectoryChanged.Unsubscribe(value!); + add => communicator.ModDirectoryChanged.Subscribe(value, Communication.ModDirectoryChanged.Priority.Api); + remove => communicator.ModDirectoryChanged.Unsubscribe(value); } public bool GetEnabledState() => config.EnableMods; - public event Action? EnabledChange + public event Action EnabledChange { add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); remove => communicator.EnabledChanged.Unsubscribe(value!); diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 02293873..9c64573f 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; namespace Penumbra.Communication; @@ -15,7 +14,7 @@ public sealed class ModDirectoryChanged() : EventWrapper + /// Api = 0, /// From d5ed4a38e4b760b369cc2cbe773a9d017ff464d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:39:12 +0200 Subject: [PATCH 029/865] Fix2? --- Penumbra.Api | 2 +- Penumbra/Api/Api/PluginStateApi.cs | 43 ++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index cd56068a..a8e2fe02 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit cd56068aac3762c7b011d13a04637a3c3f09775f +Subproject commit a8e2fe0219b8fd1f787171f11e33571317e531c1 diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index e1eec1b2..e053e56f 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -5,26 +5,41 @@ using Penumbra.Services; namespace Penumbra.Api.Api; -public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService +public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisposable { + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + + public PluginStateApi(Configuration config, CommunicatorService communicator) + { + _config = config; + _communicator = communicator; + _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChanged, Communication.ModDirectoryChanged.Priority.Api); + _communicator.EnabledChanged.Subscribe(OnEnabledChanged, EnabledChanged.Priority.Api); + } + + public void Dispose() + { + _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChanged); + _communicator.EnabledChanged.Unsubscribe(OnEnabledChanged); + } + public string GetModDirectory() - => config.ModDirectory; + => _config.ModDirectory; public string GetConfiguration() - => JsonConvert.SerializeObject(config, Formatting.Indented); + => JsonConvert.SerializeObject(_config, Formatting.Indented); - public event Action ModDirectoryChanged - { - add => communicator.ModDirectoryChanged.Subscribe(value, Communication.ModDirectoryChanged.Priority.Api); - remove => communicator.ModDirectoryChanged.Unsubscribe(value); - } + public event Action? ModDirectoryChanged; public bool GetEnabledState() - => config.EnableMods; + => _config.EnableMods; - public event Action EnabledChange - { - add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); - remove => communicator.EnabledChanged.Unsubscribe(value!); - } + public event Action? EnabledChange; + + private void OnModDirectoryChanged(string modDirectory, bool valid) + => ModDirectoryChanged?.Invoke(modDirectory, valid); + + private void OnEnabledChanged(bool value) + => EnabledChange?.Invoke(value); } From d9bd05c9ecf675da9d2d793b1c0654c8a6c5a613 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:45:25 +0200 Subject: [PATCH 030/865] Fix issue with Hex viewer. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 9599c806..a50d2aed 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9599c806877e2972f964dfa68e5207cf3a8f2b84 +Subproject commit a50d2aedbb7b2e37d30987bd0b3b96a832fdc0af From 6b5321dad8103ca3a9077f511f47047e90ccdddf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Apr 2024 14:46:26 +0200 Subject: [PATCH 031/865] Test subscription like before but without primary constructor. --- Penumbra/Api/Api/PluginStateApi.cs | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index e053e56f..d69df448 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -5,7 +5,7 @@ using Penumbra.Services; namespace Penumbra.Api.Api; -public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisposable +public class PluginStateApi : IPenumbraApiPluginState, IApiService { private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -14,14 +14,6 @@ public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisp { _config = config; _communicator = communicator; - _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChanged, Communication.ModDirectoryChanged.Priority.Api); - _communicator.EnabledChanged.Subscribe(OnEnabledChanged, EnabledChanged.Priority.Api); - } - - public void Dispose() - { - _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChanged); - _communicator.EnabledChanged.Unsubscribe(OnEnabledChanged); } public string GetModDirectory() @@ -30,16 +22,18 @@ public sealed class PluginStateApi : IPenumbraApiPluginState, IApiService, IDisp public string GetConfiguration() => JsonConvert.SerializeObject(_config, Formatting.Indented); - public event Action? ModDirectoryChanged; + public event Action? ModDirectoryChanged + { + add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); + remove => _communicator.ModDirectoryChanged.Unsubscribe(value!); + } public bool GetEnabledState() => _config.EnableMods; - public event Action? EnabledChange; - - private void OnModDirectoryChanged(string modDirectory, bool valid) - => ModDirectoryChanged?.Invoke(modDirectory, valid); - - private void OnEnabledChanged(bool value) - => EnabledChange?.Invoke(value); + public event Action? EnabledChange + { + add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); + remove => _communicator.EnabledChanged.Unsubscribe(value!); + } } From 42ad941ec22906f2d631ca22dd7dba16b16bca18 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Apr 2024 16:05:44 +0200 Subject: [PATCH 032/865] Add GetCollectionsByIdentifier. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ApiHelpers.cs | 6 ++++-- Penumbra/Api/Api/CollectionApi.cs | 19 +++++++++++++++++ Penumbra/Api/IpcProviders.cs | 1 + .../Api/IpcTester/CollectionsIpcTester.cs | 21 ++++++++++++++++++- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index a8e2fe02..2f76f42e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a8e2fe0219b8fd1f787171f11e33571317e531c1 +Subproject commit 2f76f42e54141258d89300aa78d42fb3f1878092 diff --git a/Penumbra/Api/Api/ApiHelpers.cs b/Penumbra/Api/Api/ApiHelpers.cs index 32a3956f..92a30bce 100644 --- a/Penumbra/Api/Api/ApiHelpers.cs +++ b/Penumbra/Api/Api/ApiHelpers.cs @@ -48,8 +48,10 @@ public class ApiHelpers( [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] internal static PenumbraApiEc Return(PenumbraApiEc ec, LazyString args, [CallerMemberName] string name = "Unknown") { - Penumbra.Log.Debug( - $"[{name}] Called with {args}, returned {ec}."); + if (ec is PenumbraApiEc.Success or PenumbraApiEc.NothingChanged) + Penumbra.Log.Verbose($"[{name}] Called with {args}, returned {ec}."); + else + Penumbra.Log.Debug($"[{name}] Called with {args}, returned {ec}."); return ec; } diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index de704460..e99850a6 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -10,6 +11,24 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : public Dictionary GetCollections() => collections.Storage.ToDictionary(c => c.Id, c => c.Name); + public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier) + { + if (identifier.Length == 0) + return []; + + var list = new List<(Guid Id, string Name)>(4); + if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty) + list.Add((collection.Id, collection.Name)); + else if (identifier.Length >= 8) + list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase)) + .Select(c => (c.Id, c.Name))); + + list.AddRange(collections.Storage + .Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name))) + .Select(c => (c.Id, c.Name))); + return list; + } + public Dictionary GetChangedItemsForCollection(Guid collectionId) { try diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 293af588..cc98ef0d 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -19,6 +19,7 @@ public sealed class IpcProviders : IDisposable, IApiService _providers = [ IpcSubscribers.GetCollections.Provider(pi, api.Collection), + IpcSubscribers.GetCollectionsByIdentifier.Provider(pi, api.Collection), IpcSubscribers.GetChangedItemsForCollection.Provider(pi, api.Collection), IpcSubscribers.GetCollection.Provider(pi, api.Collection), IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection), diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 12314f0c..2679bc69 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -35,7 +35,7 @@ public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService ImGuiUtil.GenericEnumCombo("Collection Type", 200, _type, out _type, t => ((CollectionType)t).ToName()); ImGui.InputInt("Object Index##Collections", ref _objectIdx, 0, 0); - ImGuiUtil.GuidInput("Collection Id##Collections", "Collection GUID...", string.Empty, ref _collectionId, ref _collectionIdString); + ImGuiUtil.GuidInput("Collection Id##Collections", "Collection Identifier...", string.Empty, ref _collectionId, ref _collectionIdString); ImGui.Checkbox("Allow Assignment Creation", ref _allowCreation); ImGui.SameLine(); ImGui.Checkbox("Allow Assignment Deletion", ref _allowDeletion); @@ -48,6 +48,25 @@ public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService if (_oldCollection != null) ImGui.TextUnformatted(!_oldCollection.HasValue ? "Created" : _oldCollection.ToString()); + IpcTester.DrawIntro(GetCollectionsByIdentifier.Label, "Collection Identifier"); + var collectionList = new GetCollectionsByIdentifier(pi).Invoke(_collectionIdString); + if (collectionList.Count == 0) + { + DrawCollection(null); + } + else + { + DrawCollection(collectionList[0]); + foreach (var pair in collectionList.Skip(1)) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + DrawCollection(pair); + } + } + IpcTester.DrawIntro(GetCollection.Label, "Current Collection"); DrawCollection(new GetCollection(pi).Invoke(ApiCollectionType.Current)); From 94b53ce7fab320286a08c2533fc400e0055eccd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 13 Apr 2024 16:06:04 +0200 Subject: [PATCH 033/865] Meh. --- Penumbra/Api/Api/CollectionApi.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index e99850a6..ff393aaf 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; From d4183a03c0734fb72e810fdbcfc2f3b33495ed1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Apr 2024 15:00:48 +0200 Subject: [PATCH 034/865] Fix bug with new empty collections. --- Penumbra/Collections/ModCollection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 327d6544..e666b151 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -25,7 +25,7 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = CreateEmpty(EmptyCollectionName, 0, 0); + public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []); /// The name of a collection. public string Name { get; set; } @@ -150,7 +150,7 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(Guid.Empty, name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], + return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); } From aeccf2b1c60074a49ec01b6430d9806323c89079 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Apr 2024 15:38:14 +0200 Subject: [PATCH 035/865] Update Submodules. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index a50d2aed..3460a817 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a50d2aedbb7b2e37d30987bd0b3b96a832fdc0af +Subproject commit 3460a817fc5e01a6b60eb834c3c59031938388fc diff --git a/Penumbra.Api b/Penumbra.Api index 2f76f42e..0c8578cf 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2f76f42e54141258d89300aa78d42fb3f1878092 +Subproject commit 0c8578cfa12bf0591ed204fd89b30b66719f678f diff --git a/Penumbra.GameData b/Penumbra.GameData index 60222d79..fe9d563d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 60222d79420662fb8e9960a66e262a380fcaf186 +Subproject commit fe9d563d9845630673cf098f7a6bfbd26e600fb4 From 0fa62f40d76c1fbbddf9f4c569a7195db811ee82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Apr 2024 16:57:23 +0200 Subject: [PATCH 036/865] Add Versions provider. --- Penumbra/Api/IpcProviders.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index cc98ef0d..21fe0a7c 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -61,6 +61,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.CopyModSettings.Provider(pi, api.ModSettings), IpcSubscribers.ApiVersion.Provider(pi, api), + new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), From 1641166d6e168d8b7e185ae0762e574cddf63201 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Apr 2024 18:15:26 +0200 Subject: [PATCH 037/865] Disable IPC listeners by default. --- Penumbra/Api/IpcTester/GameStateIpcTester.cs | 7 +++++-- Penumbra/Api/IpcTester/ModSettingsIpcTester.cs | 1 + Penumbra/Api/IpcTester/ModsIpcTester.cs | 6 ++++++ Penumbra/Api/IpcTester/PluginStateIpcTester.cs | 2 ++ Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 7 ++++--- Penumbra/Api/IpcTester/UiIpcTester.cs | 7 ++++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs index 2c41b882..93806162 100644 --- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -33,9 +33,12 @@ public class GameStateIpcTester : IUiService, IDisposable public GameStateIpcTester(DalamudPluginInterface pi) { _pi = pi; - CharacterBaseCreating = CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); - CharacterBaseCreated = CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); + CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); + CharacterBaseCreated = IpcSubscribers.CreatedCharacterBase.Subscriber(pi, UpdateLastCreated2); GameObjectResourcePathResolved = IpcSubscribers.GameObjectResourcePathResolved.Subscriber(pi, UpdateGameObjectResourcePath); + CharacterBaseCreating.Disable(); + CharacterBaseCreated.Disable(); + GameObjectResourcePathResolved.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index c33fcdee..b117d603 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -37,6 +37,7 @@ public class ModSettingsIpcTester : IUiService, IDisposable { _pi = pi; SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting); + SettingChanged.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index 878a8214..43f397e5 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -55,13 +55,19 @@ public class ModsIpcTester : IUiService, IDisposable _lastMovedModFrom = s1; _lastMovedModTo = s2; }); + DeleteSubscriber.Disable(); + AddSubscriber.Disable(); + MoveSubscriber.Disable(); } public void Dispose() { DeleteSubscriber.Dispose(); + DeleteSubscriber.Disable(); AddSubscriber.Dispose(); + AddSubscriber.Disable(); MoveSubscriber.Dispose(); + MoveSubscriber.Disable(); } public void Draw() diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index 0588e5bd..984f17b1 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -36,6 +36,8 @@ public class PluginStateIpcTester : IUiService, IDisposable Initialized = IpcSubscribers.Initialized.Subscriber(pi, AddInitialized); Disposed = IpcSubscribers.Disposed.Subscriber(pi, AddDisposed); EnabledChange = IpcSubscribers.EnabledChange.Subscriber(pi, SetLastEnabled); + ModDirectoryChanged.Disable(); + EnabledChange.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs index 281c7ad4..801f0b97 100644 --- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -22,9 +22,10 @@ public class RedrawingIpcTester : IUiService, IDisposable public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects) { - _pi = pi; - _objects = objects; - Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); + _pi = pi; + _objects = objects; + Redrawn = GameObjectRedrawn.Subscriber(_pi, SetLastRedrawn); + Redrawn.Disable(); } public void Dispose() diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index 29ddc22e..d95b79b8 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -5,7 +5,6 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; -using Penumbra.Communication; namespace Penumbra.Api.IpcTester; @@ -38,6 +37,12 @@ public class UiIpcTester : IUiService, IDisposable PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); + PreSettingsTabBar.Disable(); + PreSettingsPanel.Disable(); + PostEnabled.Disable(); + PostSettingsPanelDraw.Disable(); + ChangedItemTooltip.Disable(); + ChangedItemClicked.Disable(); } public void Dispose() From fd1f9b95d60b85d036f27addd3f7a965e815be75 Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 18 Apr 2024 21:23:18 +1000 Subject: [PATCH 038/865] Add Single2 support for UVs --- Penumbra/Import/Models/Export/MeshExporter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index d3ca87dc..c2562293 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -312,6 +312,7 @@ public class MeshExporter { return type switch { + MdlFile.VertexType.Single2 => new Vector2(reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), @@ -379,6 +380,7 @@ public class MeshExporter { MdlFile.VertexType.Half2 => 1, MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Single2 => 1, MdlFile.VertexType.Single4 => 2, _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), }; From dbfaf37800f20c202f8d01a6dacf6348ecdb7a9f Mon Sep 17 00:00:00 2001 From: ackwell Date: Thu, 18 Apr 2024 21:47:07 +1000 Subject: [PATCH 039/865] Export to .glb --- Penumbra/Import/Models/ModelManager.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 1a52c4dd..485a76a7 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -213,7 +213,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect Penumbra.Log.Debug("[GLTF Export] Saving..."); var gltfModel = scene.ToGltf2(); - gltfModel.SaveGLTF(outputPath); + gltfModel.Save(outputPath); Penumbra.Log.Debug("[GLTF Export] Done."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03f276ea..6cd9b912 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -145,8 +145,8 @@ public partial class ModEditWindow if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo || gamePath.IsEmpty)) - _fileDialog.OpenSavePicker("Save model as glTF.", ".gltf", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), - ".gltf", (valid, path) => + _fileDialog.OpenSavePicker("Save model as glTF.", ".glb", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), + ".glb", (valid, path) => { if (!valid) return; From aeb7bd5431d0e3822010305ebeeb87de5b52604b Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 19 Apr 2024 00:34:08 +1000 Subject: [PATCH 040/865] Ensure materials contain at least one / --- .../ModEditWindow.Models.MdlTab.cs | 13 ++++- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 54 +++++++++++++------ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cca8fe10..b8c0176a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -49,7 +49,7 @@ public partial class ModEditWindow /// public bool Valid - => Mdl.Valid; + => Mdl.Valid && Mdl.Materials.All(ValidateMaterial); /// public byte[] Write() @@ -285,6 +285,17 @@ public partial class ModEditWindow : _edit._gameData.GetFile(resolvedPath.InternalName.ToString())?.Data; } + /// Validate the specified material. + /// + /// While materials can be relative (`/mt_...`) or absolute (`bg/...`), + /// they invariably must contain at least one directory seperator. + /// Missing this can lead to a crash. + /// + public bool ValidateMaterial(string material) + { + return material.Contains('/'); + } + /// Remove the material given by the index. /// Meshes using the removed material are redirected to material 0, and those after the index are corrected. public void RemoveMaterial(int materialIndex) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 6cd9b912..1cfa7585 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; using OtterGui.Custom; @@ -295,7 +296,7 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Materials")) return false; - using var table = ImRaii.Table(string.Empty, disabled ? 2 : 3, ImGuiTableFlags.SizingFixedFit); + using var table = ImRaii.Table(string.Empty, disabled ? 2 : 4, ImGuiTableFlags.SizingFixedFit); if (!table) return false; @@ -305,7 +306,10 @@ public partial class ModEditWindow ImGui.TableSetupColumn("index", ImGuiTableColumnFlags.WidthFixed, 80 * UiHelpers.Scale); ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthStretch, 1); if (!disabled) + { ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("help", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + } var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; for (var materialIndex = 0; materialIndex < materials.Length; materialIndex++) @@ -321,12 +325,15 @@ public partial class ModEditWindow ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/'; ImGui.TableNextColumn(); - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) - return ret; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) + { + ret |= true; + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; + } + ImGui.TableNextColumn(); - tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); - _modelNewMaterial = string.Empty; - return true; + return ret; } private bool DrawMaterialRow(MdlTab tab, bool disabled, string[] materials, int materialIndex, ImGuiInputTextFlags inputFlags) @@ -353,20 +360,33 @@ public partial class ModEditWindow return ret; ImGui.TableNextColumn(); - // Need to have at least one material. - if (materials.Length <= 1) - return ret; + if (materials.Length > 1) + { + var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1."; + var modifierActive = _config.DeleteModModifier.IsActive(); + if (!modifierActive) + tt += $"\nHold {_config.DeleteModModifier} to delete."; - var tt = "Delete this material.\nAny meshes targeting this material will be updated to use material #1."; - var modifierActive = _config.DeleteModModifier.IsActive(); - if (!modifierActive) - tt += $"\nHold {_config.DeleteModModifier} to delete."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true)) - return ret; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, tt, !modifierActive, true)) + { + tab.RemoveMaterial(materialIndex); + ret |= true; + } + } - tab.RemoveMaterial(materialIndex); - return true; + ImGui.TableNextColumn(); + // Add markers to invalid materials. + if (!tab.ValidateMaterial(temp)) + using (var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true)) + { + ImGuiComponents.HelpMarker( + "Materials must be either relative (e.g. \"/filename.mtrl\")\n" + + "or absolute (e.g. \"chara/full/path/to/filename.mtrl\").", + FontAwesomeIcon.TimesCircle); + } + + return ret; } private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) From ef1bbb6d9d8443791a0c4d534bf373f0e490e966 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:40:12 +0200 Subject: [PATCH 041/865] I don't know what I'm doing --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 3 +- Penumbra/Import/Models/Export/MeshExporter.cs | 19 +++++----- Penumbra/Import/Models/Import/MeshImporter.cs | 13 ++++--- .../Import/Models/Import/ModelImporter.cs | 34 +++++++++--------- .../LiveColorTablePreviewer.cs | 7 ++-- .../ModEditWindow.Materials.ColorTable.cs | 35 ++++++++++--------- .../ModEditWindow.Materials.MtrlTab.cs | 9 ++--- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 9 files changed, 65 insertions(+), 59 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fe9d563d..845d1f99 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fe9d563d9845630673cf098f7a6bfbd26e600fb4 +Subproject commit 845d1f99a752f4d23288a316e42d4bfa32fa987f diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 2fa4e1b2..73a5e725 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -1,5 +1,6 @@ using Lumina.Data.Parsing; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using SharpGLTF.Materials; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; @@ -102,7 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - private readonly struct ProcessCharacterNormalOperation(Image normal, MtrlFile.ColorTable table) : IRowOperation + private readonly struct ProcessCharacterNormalOperation(Image normal, ColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); public Image BaseColor { get; } = new(normal.Width, normal.Height); diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index d3ca87dc..f372f665 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -3,6 +3,7 @@ using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.IO; @@ -55,7 +56,7 @@ public class MeshExporter private readonly byte _lod; private readonly ushort _meshIndex; - private MdlStructs.MeshStruct XivMesh + private MeshStruct XivMesh => _mdl.Meshes[_meshIndex]; private readonly MaterialBuilder _material; @@ -109,8 +110,8 @@ public class MeshExporter var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); - - foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take(xivBoneTable.BoneCount).WithIndex()) + // #TODO @ackwell maybe fix for V6 Models, I think this works fine. + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; if (!skeleton.Names.TryGetValue(boneName, out var gltfBoneIndex)) @@ -238,19 +239,15 @@ public class MeshExporter { "targetNames", shapeNames }, }); - string[] attributes = []; - var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); + string[] attributes = []; + var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); if (maxAttribute < _mdl.Attributes.Length) - { attributes = Enumerable.Range(0, 32) .Where(index => ((attributeMask >> index) & 1) == 1) .Select(index => _mdl.Attributes[index]) .ToArray(); - } else - { _notifier.Warning("Invalid attribute data, ignoring."); - } return new MeshData { @@ -278,7 +275,7 @@ public class MeshExporter for (var streamIndex = 0; streamIndex < MaximumMeshBufferStreams; streamIndex++) { streams[streamIndex] = new BinaryReader(new MemoryStream(_mdl.RemainingData)); - streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset[streamIndex]); + streams[streamIndex].Seek(_mdl.VertexOffset[_lod] + XivMesh.VertexBufferOffset(streamIndex)); } var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements @@ -315,7 +312,7 @@ public class MeshExporter MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1d4b223d..3a11cb04 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -1,5 +1,6 @@ using Lumina.Data.Parsing; using OtterGui; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -8,7 +9,7 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { public struct Mesh { - public MdlStructs.MeshStruct MeshStruct; + public MeshStruct MeshStruct; public List SubMeshStructs; public string? Material; @@ -69,10 +70,14 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) return new Mesh { - MeshStruct = new MdlStructs.MeshStruct + MeshStruct = new MeshStruct { - VertexBufferOffset = [0, (uint)_streams[0].Count, (uint)(_streams[0].Count + _streams[1].Count)], - VertexBufferStride = _strides, + VertexBufferOffset1 = 0, + VertexBufferOffset2 = (uint)_streams[0].Count, + VertexBufferOffset3 = (uint)(_streams[0].Count + _streams[1].Count), + VertexBufferStride1 = _strides[0], + VertexBufferStride2 = _strides[1], + VertexBufferStride3 = _strides[2], VertexCount = _vertexCount, VertexStreamCount = (byte)_vertexDeclaration.Value.VertexElements .Select(element => element.Stream + 1) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 8f917b0e..eedd12ab 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,6 +1,7 @@ using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; @@ -14,10 +15,11 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) } // NOTE: This is intended to match TexTool's grouping regex, ".*[_ ^]([0-9]+)[\\.\\-]?([0-9]+)?$" - [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + [GeneratedRegex(@"[_ ^](?'Mesh'[0-9]+)[.-]?(?'SubMesh'[0-9]+)?$", + RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] private static partial Regex MeshNameGroupingRegex(); - private readonly List _meshes = []; + private readonly List _meshes = []; private readonly List _subMeshes = []; private readonly List _materials = []; @@ -27,10 +29,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) private readonly List _indices = []; - private readonly List _bones = []; - private readonly List _boneTables = []; + private readonly List _bones = []; + private readonly List _boneTables = []; - private readonly BoundingBox _boundingBox = new BoundingBox(); + private readonly BoundingBox _boundingBox = new(); private readonly List _metaAttributes = []; @@ -95,9 +97,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) IndexBufferSize = (uint)indexBuffer.Length, }, ], - - Materials = [.. materials], - + Materials = [.. materials], BoundingBoxes = _boundingBox.ToStruct(), // TODO: Would be good to calculate all of this up the tree. @@ -132,9 +132,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) private void BuildMeshForGroup(IEnumerable subMeshNodes, int index) { // Record some offsets we'll be using later, before they get mutated with mesh values. - var subMeshOffset = _subMeshes.Count; - var vertexOffset = _vertexBuffer.Count; - var indexOffset = _indices.Count; + var subMeshOffset = _subMeshes.Count; + var vertexOffset = _vertexBuffer.Count; + var indexOffset = _indices.Count; var mesh = MeshImporter.Import(subMeshNodes, notifier.WithContext($"Mesh {index}")); var meshStartIndex = (uint)(mesh.MeshStruct.StartIndex + indexOffset); @@ -154,9 +154,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) SubMeshIndex = (ushort)(mesh.MeshStruct.SubMeshIndex + subMeshOffset), BoneTableIndex = boneTableIndex, StartIndex = meshStartIndex, - VertexBufferOffset = mesh.MeshStruct.VertexBufferOffset - .Select(offset => (uint)(offset + vertexOffset)) - .ToArray(), + VertexBufferOffset1 = (uint)(mesh.MeshStruct.VertexBufferOffset1 + vertexOffset), + VertexBufferOffset2 = (uint)(mesh.MeshStruct.VertexBufferOffset2 + vertexOffset), + VertexBufferOffset3 = (uint)(mesh.MeshStruct.VertexBufferOffset3 + vertexOffset), }); _boundingBox.Merge(mesh.BoundingBox); @@ -196,7 +196,8 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) // arrays, values is practically guaranteed to be the highest of the // group, so a failure on any of them will be a failure on it. if (_shapeValues.Count > ushort.MaxValue) - throw notifier.Exception($"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); + throw notifier.Exception( + $"Importing this file would require more than the maximum of {ushort.MaxValue} shape values.\nTry removing or applying shape keys that do not need to be changed at runtime in-game."); } private ushort GetMaterialIndex(string materialName) @@ -216,6 +217,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) return (ushort)count; } + // #TODO @ackwell fix for V6 Models private ushort BuildBoneTable(List boneNames) { var boneIndices = new List(); @@ -238,7 +240,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); var boneTableIndex = _boneTables.Count; - _boneTables.Add(new MdlStructs.BoneTableStruct() + _boneTables.Add(new BoneTableStruct() { BoneIndex = boneIndicesArray, BoneCount = (byte)boneIndices.Count, diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 4d35e68a..f211e0bc 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; @@ -9,7 +8,7 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { public const int TextureWidth = 4; - public const int TextureHeight = MtrlFile.ColorTable.NumRows; + public const int TextureHeight = GameData.Files.MaterialStructs.ColorTable.NumUsedRows; public const int TextureLength = TextureWidth * TextureHeight * 4; private readonly IFramework _framework; @@ -17,7 +16,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private readonly Texture** _colorTableTexture; private readonly SafeTextureHandle _originalColorTableTexture; - private bool _updatePending; + private bool _updatePending; public Half[] ColorTable { get; } @@ -40,7 +39,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - ColorTable = new Half[TextureLength]; + ColorTable = new Half[TextureLength]; _updatePending = true; framework.Update += OnFrameworkUpdate; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index a4e25f77..54c0eff6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.String.Functions; namespace Penumbra.UI.AdvancedWindow; @@ -74,7 +75,7 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + for (var i = 0; i < ColorTable.NumUsedRows; ++i) { ret |= DrawColorTableRow(tab, i, disabled); ImGui.TableNextRow(); @@ -115,8 +116,8 @@ public partial class ModEditWindow { var ret = false; if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId); + for (var i = 0; i < ColorTable.NumUsedRows; ++i) + ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); tab.UpdateColorTablePreview(); @@ -140,21 +141,21 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) + if (data.Length < Marshal.SizeOf()) return false; ref var rows = ref tab.Mtrl.Table; fixed (void* ptr = data, output = &rows) { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() + MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); + if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() && tab.Mtrl.HasDyeTable) { ref var dyeRows = ref tab.Mtrl.DyeTable; fixed (void* output2 = &dyeRows) { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); + MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), + Marshal.SizeOf()); } } } @@ -169,7 +170,7 @@ public partial class ModEditWindow } } - private static unsafe void ColorTableCopyClipboardButton(MtrlFile.ColorTable.Row row, MtrlFile.ColorDyeTable.Row dye) + private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Export this row to your clipboard.", false, true)) @@ -177,11 +178,11 @@ public partial class ModEditWindow try { - var data = new byte[MtrlFile.ColorTable.Row.Size + 2]; + var data = new byte[ColorTable.Row.Size + 2]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, MtrlFile.ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + MtrlFile.ColorTable.Row.Size, &dye, 2); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, 2); } var text = Convert.ToBase64String(data); @@ -217,15 +218,15 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != MtrlFile.ColorTable.Row.Size + 2 + if (data.Length != ColorTable.Row.Size + 2 || !tab.Mtrl.HasTable) return false; fixed (byte* ptr = data) { - tab.Mtrl.Table[rowIdx] = *(MtrlFile.ColorTable.Row*)ptr; + tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(MtrlFile.ColorDyeTable.Row*)(ptr + MtrlFile.ColorTable.Row.Size); + tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); } tab.UpdateColorTableRowPreview(rowIdx); @@ -451,7 +452,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, MtrlFile.ColorDyeTable.Row dye, float floatSize) + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) @@ -463,7 +464,7 @@ public partial class ModEditWindow var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Apply the selected dye to this row.", disabled, true); - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain); + ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); if (ret) tab.UpdateColorTableRowPreview(rowIdx); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index b4801f5f..9421493e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.GameData.Data; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.MaterialPreview; @@ -601,7 +602,7 @@ public partial class ModEditWindow var stm = _edit._stainService.StmFile; var dye = Mtrl.DyeTable[rowIdx]; if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); + row.ApplyDyeTemplate(dye, dyes, default); } if (HighlightedColorTableRow == rowIdx) @@ -628,12 +629,12 @@ public partial class ModEditWindow { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < MtrlFile.ColorTable.NumRows; ++i) + for (var i = 0; i < ColorTable.NumUsedRows; ++i) { ref var row = ref rows[i]; var dye = Mtrl.DyeTable[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); + row.ApplyDyeTemplate(dye, dyes, default); } } @@ -647,7 +648,7 @@ public partial class ModEditWindow } } - private static void ApplyHighlight(ref MtrlFile.ColorTable.Row row, float time) + private static void ApplyHighlight(ref ColorTable.Row row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03f276ea..80b1a5d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -483,7 +483,7 @@ public partial class ModEditWindow if (table) { ImGuiUtil.DrawTableColumn("Version"); - ImGuiUtil.DrawTableColumn(_lastFile.Version.ToString()); + ImGuiUtil.DrawTableColumn($"0x{_lastFile.Version:X}"); ImGuiUtil.DrawTableColumn("Radius"); ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); From 624dd40d58bc3817c3fbd271564042c58ad72439 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:46:52 +0200 Subject: [PATCH 042/865] Handle writing. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 845d1f99..aff136e2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 845d1f99a752f4d23288a316e42d4bfa32fa987f +Subproject commit aff136e2ff79990989cbe1c518a79b7b83e294a5 From 75cfffeba73e80f970d617e7d36622e709e444c3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 15:51:23 +0200 Subject: [PATCH 043/865] Oops. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index aff136e2..9208c9c2 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit aff136e2ff79990989cbe1c518a79b7b83e294a5 +Subproject commit 9208c9c242244beeb3c1fb826582d72da09831af From ceb3d39a9ac71373acc3e2d33623876488c872e8 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 01:22:03 +1000 Subject: [PATCH 044/865] Normalise _FFXIV_COLOR values Fixes xivdev/Penumbra#411 --- Penumbra/Import/Models/Export/VertexFragment.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 08b2a214..7a82e994 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -12,7 +12,7 @@ and there's reason to overhaul the export pipeline. public struct VertexColorFfxiv : IVertexCustom { // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; public int MaxColors => 0; @@ -81,7 +81,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom [VertexAttribute("TEXCOORD_0")] public Vector2 TexCoord0; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; public int MaxColors => 0; @@ -163,7 +163,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom [VertexAttribute("TEXCOORD_1")] public Vector2 TexCoord1; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, false)] + [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; public int MaxColors => 0; From 8fc7de64d9d23c9874861567dd6ada6a0f246d57 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 17:55:28 +0200 Subject: [PATCH 045/865] Start group rework. --- Penumbra/Collections/Cache/CollectionCache.cs | 50 +++++-------------- .../Collections/Cache/CollectionModData.cs | 10 ++-- Penumbra/Mods/Editor/IMod.cs | 14 +++++- Penumbra/Mods/Mod.cs | 19 +++++++ Penumbra/Mods/Subclasses/IModGroup.cs | 4 ++ Penumbra/Mods/Subclasses/ISubMod.cs | 10 ++++ Penumbra/Mods/Subclasses/MultiModGroup.cs | 11 ++++ Penumbra/Mods/Subclasses/SingleModGroup.cs | 5 ++ Penumbra/Mods/TemporaryMod.cs | 28 ++++++++++- 9 files changed, 106 insertions(+), 45 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e1b32204..ded1dc73 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -2,12 +2,10 @@ using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; namespace Penumbra.Collections.Cache; @@ -231,37 +229,12 @@ public sealed class CollectionCache : IDisposable /// Add all files and possibly manipulations of a given mod according to its settings in this collection. internal void AddModSync(IMod mod, bool addMetaChanges) { - if (mod.Index >= 0) - { - var settings = _collection[mod.Index].Settings; - if (settings is not { Enabled: true }) - return; + var files = GetFiles(mod); + foreach (var (path, file) in files.FileRedirections) + AddFile(path, file, mod); - foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) - { - if (group.Count == 0) - continue; - - var config = settings.Settings[groupIndex]; - switch (group) - { - case SingleModGroup single: - AddSubMod(single[config.AsIndex], mod); - break; - case MultiModGroup multi: - { - foreach (var (option, _) in multi.WithIndex() - .Where(p => config.HasFlag(p.Index)) - .OrderByDescending(p => group.OptionPriority(p.Index))) - AddSubMod(option, mod); - - break; - } - } - } - } - - AddSubMod(mod.Default, mod); + foreach (var manip in files.Manipulations) + AddManipulation(manip, mod); if (addMetaChanges) { @@ -273,14 +246,15 @@ public sealed class CollectionCache : IDisposable } } - // Add all files and possibly manipulations of a specific submod - private void AddSubMod(ISubMod subMod, IMod parentMod) + private AppliedModData GetFiles(IMod mod) { - foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps)) - AddFile(path, file, parentMod); + if (mod.Index < 0) + return mod.GetData(); - foreach (var manip in subMod.Manipulations) - AddManipulation(manip, parentMod); + var settings = _collection[mod.Index].Settings; + return settings is not { Enabled: true } + ? AppliedModData.Empty + : mod.GetData(settings); } /// Invoke only if not in a full recalculation. diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index 3a3afad2..d0a3bc76 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -1,10 +1,12 @@ using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; +/// +/// Contains associations between a mod and the paths and meta manipulations affected by that mod. +/// public class CollectionModData { private readonly Dictionary, HashSet)> _data = new(); @@ -17,7 +19,7 @@ public class CollectionModData if (_data.Remove(mod, out var data)) return data; - return (Array.Empty(), Array.Empty()); + return ([], []); } public void AddPath(IMod mod, Utf8GamePath path) @@ -28,7 +30,7 @@ public class CollectionModData } else { - data = (new HashSet { path }, new HashSet()); + data = ([path], []); _data.Add(mod, data); } } @@ -41,7 +43,7 @@ public class CollectionModData } else { - data = (new HashSet(), new HashSet { manipulation }); + data = ([], [manipulation]); _data.Add(mod, data); } } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index d3bc19b0..8b5b65e1 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,15 +1,27 @@ using OtterGui.Classes; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Subclasses; +using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; +public record struct AppliedModData( + IReadOnlyCollection> FileRedirections, + IReadOnlyCollection Manipulations) +{ + public static readonly AppliedModData Empty = new([], []); +} + public interface IMod { LowerString Name { get; } - public int Index { get; } + public int Index { get; } public ModPriority Priority { get; } + public AppliedModData GetData(ModSettings? settings = null); + + public ISubMod Default { get; } public IReadOnlyList Groups { get; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index b7d1186d..3c996c8f 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,7 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.Collections.Cache; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; @@ -59,6 +61,23 @@ public sealed class Mod : IMod public readonly SubMod Default; public readonly List Groups = []; + public AppliedModData GetData(ModSettings? settings = null) + { + if (settings is not { Enabled: true }) + return AppliedModData.Empty; + + var dictRedirections = new Dictionary(TotalFileCount); + var setManips = new HashSet(TotalManipulations); + foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) + { + var config = settings.Settings[groupIndex]; + group.AddData(config, dictRedirections, setManips); + } + + ((ISubMod)Default).AddData(dictRedirections, setManips); + return new AppliedModData(dictRedirections, setManips); + } + ISubMod IMod.Default => Default; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 2daf31e6..57ef4e98 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; using Penumbra.Services; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -24,6 +26,8 @@ public interface IModGroup : IReadOnlyCollection public bool MoveOption(int optionIdxFrom, int optionIdxTo); public void UpdatePositions(int from = 0); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs index 29323c1d..e997e07d 100644 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ b/Penumbra/Mods/Subclasses/ISubMod.cs @@ -14,6 +14,16 @@ public interface ISubMod public IReadOnlyDictionary FileSwaps { get; } public IReadOnlySet Manipulations { get; } + public void AddData(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } + public bool IsDefault { get; } public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 7479cd54..266d3037 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -5,6 +5,8 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -110,6 +112,15 @@ public sealed class MultiModGroup : IModGroup o.SetPosition(o.GroupIdx, i); } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) + { + if (setting.HasFlag(index)) + ((ISubMod)option.Mod).AddData(redirections, manipulations); + } + } + public Setting FixSetting(Setting setting) => new(setting.Value & ((1ul << Count) - 1)); } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 74769c7e..f797a709 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -3,6 +3,8 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; @@ -114,6 +116,9 @@ public sealed class SingleModGroup : IModGroup o.SetPosition(o.GroupIdx, i); } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + => this[setting.AsIndex].AddData(redirections, manipulations); + public Setting FixSetting(Setting setting) => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6be07881..8f27e201 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -20,6 +20,28 @@ public class TemporaryMod : IMod public readonly SubMod Default; + public AppliedModData GetData(ModSettings? settings = null) + { + Dictionary dict; + if (Default.FileSwapData.Count == 0) + { + dict = Default.FileData; + } + else if (Default.FileData.Count == 0) + { + dict = Default.FileSwapData; + } + else + { + // Need to ensure uniqueness. + dict = new Dictionary(Default.FileData.Count + Default.FileSwaps.Count); + foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) + dict.TryAdd(gamePath, file); + } + + return new AppliedModData(dict, Default.Manipulations); + } + ISubMod IMod.Default => Default; @@ -53,7 +75,8 @@ public class TemporaryMod : IMod dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", null, null); + $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", + null, null); var mod = new Mod(dir); var defaultMod = mod.Default; foreach (var (gamePath, fullPath) in collection.ResolvedFiles) @@ -86,7 +109,8 @@ public class TemporaryMod : IMod saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); - Penumbra.Log.Information($"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); + Penumbra.Log.Information( + $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } catch (Exception e) { From 9f4c6767f822be23632b39e3ab73792d19290ec3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Apr 2024 18:28:25 +0200 Subject: [PATCH 046/865] Remove ISubMod. --- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 7 +- Penumbra/Mods/Editor/FileRegistry.cs | 2 +- Penumbra/Mods/Editor/IMod.cs | 12 ++-- Penumbra/Mods/Editor/ModEditor.cs | 4 +- Penumbra/Mods/Editor/ModFileCollection.cs | 14 ++-- Penumbra/Mods/Editor/ModFileEditor.cs | 16 ++--- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 15 ++--- Penumbra/Mods/Manager/ModOptionEditor.cs | 15 ++--- Penumbra/Mods/Mod.cs | 13 ++-- Penumbra/Mods/ModCreator.cs | 8 +-- Penumbra/Mods/Subclasses/IModGroup.cs | 12 ++-- Penumbra/Mods/Subclasses/ISubMod.cs | 67 ------------------- Penumbra/Mods/Subclasses/ModSettings.cs | 35 +--------- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 +- Penumbra/Mods/Subclasses/SingleModGroup.cs | 4 +- Penumbra/Mods/Subclasses/SubMod.cs | 58 ++++++++++++++-- Penumbra/Mods/TemporaryMod.cs | 5 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 4 +- .../ModEditWindow.QuickImport.cs | 2 +- 23 files changed, 123 insertions(+), 184 deletions(-) delete mode 100644 Penumbra/Mods/Subclasses/ISubMod.cs diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 099b133c..f4b7d47e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -152,7 +152,7 @@ public partial class TexToolsImporter } // Iterate through all pages - var options = new List(); + var options = new List(); var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index c8530936..938199aa 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } - public void DeleteDuplicates(ModFileCollection files, Mod mod, ISubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) { if (!Worker.IsCompleted || _duplicates.Count == 0) return; @@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -86,8 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } else { - var sub = (SubMod)subMod; - sub.FileData = dict; + subMod.FileData = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 96d027b3..427c58ca 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -5,7 +5,7 @@ namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(ISubMod, Utf8GamePath)> SubModUsage = []; + public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; public FullPath File { get; private init; } public Utf8RelPath RelPath { get; private init; } public long FileSize { get; private init; } diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 8b5b65e1..c4c4be2f 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -6,8 +6,8 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; public record struct AppliedModData( - IReadOnlyCollection> FileRedirections, - IReadOnlyCollection Manipulations) + Dictionary FileRedirections, + HashSet Manipulations) { public static readonly AppliedModData Empty = new([], []); } @@ -19,14 +19,10 @@ public interface IMod public int Index { get; } public ModPriority Priority { get; } + public IReadOnlyList Groups { get; } + public AppliedModData GetData(ModSettings? settings = null); - - public ISubMod Default { get; } - public IReadOnlyList Groups { get; } - - public IEnumerable AllSubMods { get; } - // Cache public int TotalManipulations { get; } } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index b22aea17..d9781c06 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -29,7 +29,7 @@ public class ModEditor( public int OptionIdx { get; private set; } public IModGroup? Group { get; private set; } - public ISubMod? Option { get; private set; } + public SubMod? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); @@ -104,7 +104,7 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllOptions(Mod mod, Action action) { action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 2f8bdfb1..9dd78217 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public void UpdateAll(Mod mod, ISubMod option) + public void UpdateAll(Mod mod, SubMod option) { UpdateFiles(mod, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken()); } - public void UpdatePaths(Mod mod, ISubMod option) + public void UpdatePaths(Mod mod, SubMod option) => UpdatePaths(mod, option, true, new CancellationToken()); public void Clear() @@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable public void ClearMissingFiles() => _missing.Clear(); - public void RemoveUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Remove(gamePath); if (file != null) @@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable } } - public void RemoveUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - public void AddUsedPath(ISubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Add(gamePath); if (file == null) @@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable file.SubModUsage.Add((option, gamePath)); } - public void AddUsedPath(ISubMod option, FullPath file, Utf8GamePath gamePath) + public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) @@ -154,7 +154,7 @@ public class ModFileCollection : IDisposable _usedPaths.Clear(); } - private void UpdatePaths(Mod mod, ISubMod option, bool clearRegistries, CancellationToken tok) + private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) { tok.ThrowIfCancellationRequested(); ClearPaths(clearRegistries, tok); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 30e97093..4bdf4b1b 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -30,16 +30,16 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu return num; } - public void Revert(Mod mod, ISubMod option) + public void Revert(Mod mod, SubMod option) { files.UpdateAll(mod, option); Changes = false; } /// Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths(Mod mod, ISubMod option) + public void RemoveMissingPaths(Mod mod, SubMod option) { - void HandleSubMod(ISubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -61,7 +61,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// If path is empty, it will be deleted instead. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// - public bool SetGamePath(ISubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) { if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; @@ -84,7 +84,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. /// - public int AddPathsToSelected(ISubMod option, IEnumerable files1, int skipFolders = 0) + public int AddPathsToSelected(SubMod option, IEnumerable files1, int skipFolders = 0) { var failed = 0; foreach (var file in files1) @@ -111,7 +111,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(ISubMod option, IEnumerable files1) + public void RemovePathsFromSelected(SubMod option, IEnumerable files1) { foreach (var file in files1) { @@ -129,7 +129,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, ISubMod option, IEnumerable files1) + public void DeleteFiles(Mod mod, SubMod option, IEnumerable files1) { var deletions = 0; foreach (var file in files1) @@ -155,7 +155,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } - private bool CheckAgainstMissing(Mod mod, ISubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) { if (!files.Missing.Contains(file)) return true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 842b1bb3..25590c49 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -151,7 +151,7 @@ public class ModMerger : IDisposable MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) + private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) { var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 31aefdf5..a6218c6f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager) Changes = true; } - public void Load(Mod mod, ISubMod currentOption) + public void Load(Mod mod, SubMod currentOption) { OtherImcCount = 0; OtherEqpCount = 0; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index ada06264..0d5f05a9 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager) public IReadOnlyDictionary Swaps => _swaps; - public void Revert(ISubMod option) + public void Revert(SubMod option) { _swaps.SetTo(option.FileSwaps); Changes = false; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index e229738d..21b9ef2c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; @@ -15,14 +16,13 @@ public class ItemSwapContainer private readonly MetaFileManager _manager; private readonly ObjectIdentification _identifier; - private Dictionary _modRedirections = []; - private HashSet _modManipulations = []; + private AppliedModData _appliedModData = AppliedModData.Empty; public IReadOnlyDictionary ModRedirections - => _modRedirections; + => _appliedModData.FileRedirections; public IReadOnlySet ModManipulations - => _modManipulations; + => _appliedModData.Manipulations; public readonly List Swaps = []; @@ -97,12 +97,11 @@ public class ItemSwapContainer Clear(); if (mod == null || mod.Index < 0) { - _modRedirections = []; - _modManipulations = []; + _appliedModData = AppliedModData.Empty; } else { - (_modRedirections, _modManipulations) = ModSettings.GetResolveData(mod, settings); + _appliedModData = ModSettings.GetResolveData(mod, settings); } } @@ -120,7 +119,7 @@ public class ItemSwapContainer private Func MetaResolver(ModCollection? collection) { - var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _modManipulations; + var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _appliedModData.Manipulations; return m => set.TryGetValue(m, out var a) ? a : m; } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 9efb8a3f..07c6f38e 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -262,15 +262,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add an existing option to a given group with default priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option) + public void AddOption(Mod mod, int groupIdx, SubMod option) => AddOption(mod, groupIdx, option, ModPriority.Default); /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, ISubMod option, ModPriority priority) + public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) { - if (option is not SubMod o) - return; - var group = mod.Groups[groupIdx]; switch (group) { @@ -280,12 +277,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: - o.SetPosition(groupIdx, s.Count); - s.OptionData.Add(o); + option.SetPosition(groupIdx, s.Count); + s.OptionData.Add(option); break; case MultiModGroup m: - o.SetPosition(groupIdx, m.Count); - m.PrioritizedOptions.Add((o, priority)); + option.SetPosition(groupIdx, m.Count); + m.PrioritizedOptions.Add((option, priority)); break; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 3c996c8f..25f3c510 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -32,6 +32,9 @@ public sealed class Mod : IMod public ModPriority Priority => ModPriority.Default; + IReadOnlyList IMod.Groups + => Groups; + internal Mod(DirectoryInfo modPath) { ModPath = modPath; @@ -74,18 +77,12 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - ((ISubMod)Default).AddData(dictRedirections, setManips); + Default.AddData(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } - ISubMod IMod.Default - => Default; - - IReadOnlyList IMod.Groups - => Groups; - public IEnumerable AllSubMods - => Groups.SelectMany(o => o).OfType().Prepend(Default); + => Groups.SelectMany(o => o).Prepend(Default); public List FindUnusedFiles() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 2bcdd3b1..661dd6fb 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -235,7 +235,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +248,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange(subMods.OfType().Select((s, idx) => (s, new ModPriority(idx)))); + group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -269,7 +269,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, } /// Create the data for a given sub mod from its data and the folder it is based on. - public ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) { var list = optionFolder.EnumerateNonHiddenFiles() .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) @@ -288,7 +288,7 @@ public partial class ModCreator(SaveService _saveService, Configuration config, } /// Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod(string name) + internal static SubMod CreateEmptySubMod(string name) => new SubMod(null!) // Mod is irrelevant here, only used for saving. { Name = name, diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 57ef4e98..3f363542 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -6,7 +6,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IReadOnlyCollection +public interface IModGroup : IReadOnlyCollection { public const int MaxMultiOptions = 63; @@ -18,7 +18,7 @@ public interface IModGroup : IReadOnlyCollection public ModPriority OptionPriority(Index optionIdx); - public ISubMod this[Index idx] { get; } + public SubMod this[Index idx] { get; } public bool IsOption { get; } @@ -37,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; - private readonly ISubMod? _defaultMod; + private readonly SubMod? _defaultMod; private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) @@ -59,7 +59,7 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, ISubMod @default, bool onlyAscii) + public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; @@ -91,7 +91,7 @@ public readonly struct ModSaveGroup : ISavable j.WriteStartArray(); for (var idx = 0; idx < _group.Count; ++idx) { - ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch + SubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch { GroupType.Multi => _group.OptionPriority(idx), _ => null, @@ -103,7 +103,7 @@ public readonly struct ModSaveGroup : ISavable } else { - ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); + SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); } } } diff --git a/Penumbra/Mods/Subclasses/ISubMod.cs b/Penumbra/Mods/Subclasses/ISubMod.cs deleted file mode 100644 index e997e07d..00000000 --- a/Penumbra/Mods/Subclasses/ISubMod.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.Subclasses; - -public interface ISubMod -{ - public string Name { get; } - public string FullName { get; } - public string Description { get; } - - public IReadOnlyDictionary Files { get; } - public IReadOnlyDictionary FileSwaps { get; } - public IReadOnlySet Manipulations { get; } - - public void AddData(Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); - } - - public bool IsDefault { get; } - - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, ISubMod mod, DirectoryInfo basePath, ModPriority? priority) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(Name)); - j.WriteValue(mod.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(mod.Description); - if (priority != null) - { - j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value.Value); - } - - j.WritePropertyName(nameof(mod.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.Manipulations)); - serializer.Serialize(j, mod.Manipulations); - j.WriteEndObject(); - } -} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 380b242c..81a3bb41 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -2,6 +2,7 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.String.Classes; @@ -34,44 +35,14 @@ public class ModSettings }; // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. - public static (Dictionary, HashSet) GetResolveData(Mod mod, ModSettings? settings) + public static AppliedModData GetResolveData(Mod mod, ModSettings? settings) { if (settings == null) settings = DefaultSettings(mod); else settings.Settings.FixSize(mod); - var dict = new Dictionary(); - var set = new HashSet(); - - foreach (var (group, index) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) - { - if (group.Type is GroupType.Single) - { - if (group.Count > 0) - AddOption(group[settings.Settings[index].AsIndex]); - } - else - { - foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index))) - { - if (settings.Settings[index].HasFlag(optionIdx)) - AddOption(option); - } - } - } - - AddOption(mod.Default); - return (dict, set); - - void AddOption(ISubMod option) - { - foreach (var (path, file) in option.Files.Concat(option.FileSwaps)) - dict.TryAdd(path, file); - - foreach (var manip in option.Manipulations) - set.Add(manip); - } + return mod.GetData(settings); } // Automatically react to changes in a mods available options. diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 266d3037..1600072e 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -24,7 +24,7 @@ public sealed class MultiModGroup : IModGroup public ModPriority OptionPriority(Index idx) => PrioritizedOptions[idx].Priority; - public ISubMod this[Index idx] + public SubMod this[Index idx] => PrioritizedOptions[idx].Mod; public bool IsOption @@ -36,7 +36,7 @@ public sealed class MultiModGroup : IModGroup public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -117,7 +117,7 @@ public sealed class MultiModGroup : IModGroup foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) { if (setting.HasFlag(index)) - ((ISubMod)option.Mod).AddData(redirections, manipulations); + option.Mod.AddData(redirections, manipulations); } } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index f797a709..2d49fd1f 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -24,7 +24,7 @@ public sealed class SingleModGroup : IModGroup public ModPriority OptionPriority(Index _) => Priority; - public ISubMod this[Index idx] + public SubMod this[Index idx] => OptionData[idx]; public bool IsOption @@ -34,7 +34,7 @@ public sealed class SingleModGroup : IModGroup public int Count => OptionData.Count; - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() => OptionData.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 4f35cd33..386910e5 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -15,7 +16,7 @@ namespace Penumbra.Mods.Subclasses; /// Nothing is checked for existence or validity when loading. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// -public sealed class SubMod : ISubMod +public sealed class SubMod { public string Name { get; set; } = "Default"; @@ -29,7 +30,17 @@ public sealed class SubMod : ISubMod internal int OptionIdx { get; private set; } public bool IsDefault - => GroupIdx < 0; + => GroupIdx < 0; + + public void AddData(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } public Dictionary FileData = []; public Dictionary FileSwapData = []; @@ -60,8 +71,8 @@ public sealed class SubMod : ISubMod ManipulationData.Clear(); // Every option has a name, but priorities are only relevant for multi group options. - Name = json[nameof(ISubMod.Name)]?.ToObject() ?? string.Empty; - Description = json[nameof(ISubMod.Description)]?.ToObject() ?? string.Empty; + Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + Description = json[nameof(Description)]?.ToObject() ?? string.Empty; priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; var files = (JObject?)json[nameof(Files)]; @@ -104,4 +115,43 @@ public sealed class SubMod : ISubMod } } } + + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) + { + j.WriteStartObject(); + j.WritePropertyName(nameof(Name)); + j.WriteValue(mod.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(mod.Description); + if (priority != null) + { + j.WritePropertyName(nameof(IModGroup.Priority)); + j.WriteValue(priority.Value.Value); + } + + j.WritePropertyName(nameof(mod.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in mod.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(mod.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in mod.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(mod.Manipulations)); + serializer.Serialize(j, mod.Manipulations); + j.WriteEndObject(); + } } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 8f27e201..41c1211f 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -39,12 +39,9 @@ public class TemporaryMod : IMod dict.TryAdd(gamePath, file); } - return new AppliedModData(dict, Default.Manipulations); + return new AppliedModData(dict, Default.ManipulationData); } - ISubMod IMod.Default - => Default; - public IReadOnlyList Groups => Array.Empty(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index c8db7770..f765b47e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -192,7 +192,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -228,7 +228,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, FileRegistry registry, ISubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 10956deb..4ecacece 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -227,7 +227,7 @@ public partial class ModEditWindow return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod, bool replaceNonAscii) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; From 2d5afde61274f3602f15252eb64efbcb899c5cae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:02:30 +0200 Subject: [PATCH 047/865] Fix group priority writing. --- Penumbra/Mods/Subclasses/IModGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 3f363542..7554f6dc 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -82,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable j.WritePropertyName(nameof(_group.Description)); j.WriteValue(_group.Description); j.WritePropertyName(nameof(_group.Priority)); - j.WriteValue(_group.Priority); + j.WriteValue(_group.Priority.Value); j.WritePropertyName(nameof(Type)); j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); From f86f29b44a4d3dc78929107a9c99538b0d314d6a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:03:50 +0200 Subject: [PATCH 048/865] Some fixes. --- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +-- Penumbra/Mods/Subclasses/IModGroup.cs | 33 ++++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 07c6f38e..9d942574 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -158,10 +158,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { var group = mod.Groups[groupIdx]; var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) + if (option.Description == newDescription) return; - s.Description = newDescription; + option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 7554f6dc..38f070b3 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -72,8 +72,9 @@ public readonly struct ModSaveGroup : ISavable public void Save(StreamWriter writer) { - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; if (_groupIdx >= 0) { j.WriteStartObject(); @@ -87,19 +88,25 @@ public readonly struct ModSaveGroup : ISavable j.WriteValue(_group.Type.ToString()); j.WritePropertyName(nameof(_group.DefaultSettings)); j.WriteValue(_group.DefaultSettings.Value); - j.WritePropertyName("Options"); - j.WriteStartArray(); - for (var idx = 0; idx < _group.Count; ++idx) + switch (_group) { - SubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type switch - { - GroupType.Multi => _group.OptionPriority(idx), - _ => null, - }); + case SingleModGroup single: + j.WritePropertyName("Options"); + j.WriteStartArray(); + foreach (var option in single.OptionData) + SubMod.WriteSubMod(j, serializer, option, _basePath, null); + j.WriteEndArray(); + j.WriteEndObject(); + break; + case MultiModGroup multi: + j.WritePropertyName("Options"); + j.WriteStartArray(); + foreach (var (option, priority) in multi.PrioritizedOptions) + SubMod.WriteSubMod(j, serializer, option, _basePath, priority); + j.WriteEndArray(); + j.WriteEndObject(); + break; } - - j.WriteEndArray(); - j.WriteEndObject(); } else { From b99a809eba50019fad9aa7e1b25ce73c5ebca6fa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Apr 2024 11:26:12 +0200 Subject: [PATCH 049/865] Remove OptionPriority from general option groups. --- Penumbra/Mods/Subclasses/IModGroup.cs | 4 ++-- Penumbra/Mods/Subclasses/MultiModGroup.cs | 6 ++++-- Penumbra/Mods/Subclasses/SingleModGroup.cs | 6 ++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 13 ++++++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 10 +++++++--- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 38f070b3..96d7c6b7 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -16,7 +16,7 @@ public interface IModGroup : IReadOnlyCollection public ModPriority Priority { get; } public Setting DefaultSettings { get; set; } - public ModPriority OptionPriority(Index optionIdx); + public FullPath? FindBestMatch(Utf8GamePath gamePath); public SubMod this[Index idx] { get; } @@ -37,7 +37,7 @@ public readonly struct ModSaveGroup : ISavable private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; - private readonly SubMod? _defaultMod; + private readonly SubMod? _defaultMod; private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 1600072e..02ae07f4 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -21,8 +21,10 @@ public sealed class MultiModGroup : IModGroup public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public ModPriority OptionPriority(Index idx) - => PrioritizedOptions[idx].Priority; + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => PrioritizedOptions.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) + .FirstOrDefault(); public SubMod this[Index idx] => PrioritizedOptions[idx].Mod; diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 2d49fd1f..b854d2b1 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -21,8 +21,10 @@ public sealed class SingleModGroup : IModGroup public readonly List OptionData = []; - public ModPriority OptionPriority(Index _) - => Priority; + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => OptionData + .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) + .FirstOrDefault(); public SubMod this[Index idx] => OptionData[idx]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6cf24f62..a70da628 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -540,14 +540,17 @@ public partial class ModEditWindow : Window, IDisposable return currentFile.Value; if (Mod != null) - foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority) - .SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value)) - .Append(Mod.Default)) + { + foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority)) { - if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) - return value; + if (option.FindBestMatch(path) is { } fullPath) + return fullPath; } + if (Mod.Default.Files.TryGetValue(path, out var value) || Mod.Default.FileSwaps.TryGetValue(path, out value)) + return value; + } + return new FullPath(path); } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 80af7b15..b002dedd 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -532,10 +532,10 @@ public class ModPanelEditTab( panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); - if (group.Type != GroupType.Multi) + if (group is not MultiModGroup multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, 50 * UiHelpers.Scale)) panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); @@ -613,7 +613,11 @@ public class ModPanelEditTab( var sourceGroup = panel._mod.Groups[sourceGroupIdx]; var currentCount = group.Count; var option = sourceGroup[sourceOption]; - var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); + var priority = sourceGroup switch + { + MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, + _ => ModPriority.Default, + }; panel._delayedActions.Enqueue(() => { panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); From 4a6d94f0fb817810d14a9920130db7bada5ff47c Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 20:02:43 +1000 Subject: [PATCH 050/865] Avoid inclusion of zero-weighted bones in name mapping --- Penumbra/Import/Models/Import/MeshImporter.cs | 18 ++++++++++++++---- .../Import/Models/Import/VertexAttribute.cs | 19 +++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1d4b223d..5d5df948 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -189,15 +189,25 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0") - ?? throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); + var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); + var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); + + if (jointsAccessor == null || weightsAccessor == null) + throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. - // TODO: Would be neat to omit 0-weighted joints here, but doing so will require some further work on bone mapping behavior to ensure the unweighted joints can still be resolved to valid bone indices during vertex data construction. - foreach (var joints in jointsAccessor.AsVector4Array()) + for (var i = 0; i < jointsAccessor.Count; i++) { + var joints = jointsAccessor[i]; + var weights = weightsAccessor[i]; for (var index = 0; index < 4; index++) + { + // If a joint has absolutely no weight, we omit the bone entirely. + if (weights[index] == 0) + continue; + usedJoints.Add((ushort)joints[index]); + } } } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 3cfedd6f..b7f5dcf1 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -144,10 +144,10 @@ public class VertexAttribute public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { - if (!accessors.TryGetValue("JOINTS_0", out var accessor)) + if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor)) return null; - if (!accessors.ContainsKey("WEIGHTS_0")) + if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor)) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) @@ -160,18 +160,21 @@ public class VertexAttribute Usage = (byte)MdlFile.VertexUsage.BlendIndices, }; - var values = accessor.AsVector4Array(); + var joints = jointsAccessor.AsVector4Array(); + var weights = weightsAccessor.AsVector4Array(); return new VertexAttribute( element, index => { - var gltfIndices = values[index]; + var gltfIndices = joints[index]; + var gltfWeights = weights[index]; + return BuildUByte4(new Vector4( - boneMap[(ushort)gltfIndices.X], - boneMap[(ushort)gltfIndices.Y], - boneMap[(ushort)gltfIndices.Z], - boneMap[(ushort)gltfIndices.W] + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] )); } ); From 11acd7d3f46534ce445ef3700849f1f2c706491e Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 20:53:55 +1000 Subject: [PATCH 051/865] Prevent import failure when no materials are present --- Penumbra/Import/Models/Import/PrimitiveImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Import/PrimitiveImporter.cs b/Penumbra/Import/Models/Import/PrimitiveImporter.cs index 0c2968df..5df7597e 100644 --- a/Penumbra/Import/Models/Import/PrimitiveImporter.cs +++ b/Penumbra/Import/Models/Import/PrimitiveImporter.cs @@ -65,7 +65,7 @@ public class PrimitiveImporter ArgumentNullException.ThrowIfNull(_indices); ArgumentNullException.ThrowIfNull(_shapeValues); - var material = _primitive.Material.Name; + var material = _primitive.Material?.Name; if (material == "") material = null; From cc2f72b73df1218c094c45f9cfea3e5fa17ce534 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 20 Apr 2024 20:55:04 +1000 Subject: [PATCH 052/865] Use bg/ for absolute path example --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 1cfa7585..1f4607cf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -382,7 +382,7 @@ public partial class ModEditWindow { ImGuiComponents.HelpMarker( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" - + "or absolute (e.g. \"chara/full/path/to/filename.mtrl\").", + + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\").", FontAwesomeIcon.TimesCircle); } From 1bc3bb17c9b8a95cbe6c3edd8cf7912f45b174f2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 22 Apr 2024 23:45:30 +1000 Subject: [PATCH 053/865] Fix havok parsing for non-ANSI user paths Also improve parsing because otter is better at c# than me --- Penumbra/Import/Models/HavokConverter.cs | 10 ++++------ Penumbra/Import/Models/SkeletonConverter.cs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 89f9ac4f..38c8749a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -71,8 +71,7 @@ public static unsafe class HavokConverter /// Path to a file on the filesystem. private static hkResource* Read(string filePath) { - var path = Marshal.StringToHGlobalAnsi(filePath); - + var path = Encoding.UTF8.GetBytes(filePath); var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; @@ -81,8 +80,7 @@ public static unsafe class HavokConverter loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); // TODO: probably can use LoadFromBuffer for this. - var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); - return resource; + return hkSerializeUtil.LoadFromFile(path, null, loadOptions); } /// Serializes an hkResource* to a temporary file. @@ -94,9 +92,9 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); + var path = Encoding.UTF8.GetBytes(tempFile); var oStream = new hkOstream(); - oStream.Ctor((byte*)path); + oStream.Ctor(path); var result = stackalloc hkResult[1]; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 7058a159..25e74332 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -84,9 +84,8 @@ public static class SkeletonConverter .Where(n => n.NodeType != XmlNodeType.Comment) .Select(n => { - var text = n.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this I mean seriously - return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); + var text = n.InnerText.AsSpan().Trim()[1..]; + return BitConverter.Int32BitsToSingle(int.Parse(text, NumberStyles.HexNumber)); }) .ToArray(); From c276f922a53fc5798a9ae71673614a54bc88d9df Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Apr 2024 18:22:03 +0200 Subject: [PATCH 054/865] Update API. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 0c8578cf..590629df 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 0c8578cfa12bf0591ed204fd89b30b66719f678f +Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a From b34114400fab2f83b119ddb1ac2d8c2ff7e4a708 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:09:53 +0200 Subject: [PATCH 055/865] Fix Havok ANSI / UTF8 Issue. --- Penumbra/Import/Models/HavokConverter.cs | 10 ++++------ Penumbra/Import/Models/SkeletonConverter.cs | 5 ++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 89f9ac4f..dc9d3e6a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -71,8 +71,7 @@ public static unsafe class HavokConverter /// Path to a file on the filesystem. private static hkResource* Read(string filePath) { - var path = Marshal.StringToHGlobalAnsi(filePath); - + var path = Encoding.UTF8.GetBytes(filePath); var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance(); var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1]; @@ -81,8 +80,7 @@ public static unsafe class HavokConverter loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); // TODO: probably can use LoadFromBuffer for this. - var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); - return resource; + return hkSerializeUtil.LoadFromFile(path, null, loadOptions); } /// Serializes an hkResource* to a temporary file. @@ -94,9 +92,9 @@ public static unsafe class HavokConverter ) { var tempFile = CreateTempFile(); - var path = Marshal.StringToHGlobalAnsi(tempFile); + var path = Encoding.UTF8.GetBytes(tempFile); var oStream = new hkOstream(); - oStream.Ctor((byte*)path); + oStream.Ctor(path); var result = stackalloc hkResult[1]; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 7058a159..25e74332 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -84,9 +84,8 @@ public static class SkeletonConverter .Where(n => n.NodeType != XmlNodeType.Comment) .Select(n => { - var text = n.InnerText.Trim()[1..]; - // TODO: surely there's a less shit way to do this I mean seriously - return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber))); + var text = n.InnerText.AsSpan().Trim()[1..]; + return BitConverter.Int32BitsToSingle(int.Parse(text, NumberStyles.HexNumber)); }) .ToArray(); From e21c9fb6d1d71e0f952d416a8d1b0e817e450826 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:11:09 +0200 Subject: [PATCH 056/865] Fix some IPC stuff. --- Penumbra/Api/IpcProviders.cs | 5 +++-- Penumbra/Api/IpcTester/UiIpcTester.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 21fe0a7c..ebf71176 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -62,6 +62,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ApiVersion.Provider(pi, api), new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility + new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), @@ -99,9 +100,9 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), IpcSubscribers.PreSettingsTabBarDraw.Provider(pi, api.Ui), - IpcSubscribers.PreSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PreSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.PostEnabledDraw.Provider(pi, api.Ui), - IpcSubscribers.PostSettingsPanelDraw.Provider(pi, api.Ui), + IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), ]; diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index d95b79b8..a2c36938 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -32,9 +32,9 @@ public class UiIpcTester : IUiService, IDisposable { _pi = pi; PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); - PreSettingsPanel = IpcSubscribers.PreSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PreSettingsPanel = IpcSubscribers.PreSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); PostEnabled = IpcSubscribers.PostEnabledDraw.Subscriber(pi, UpdateLastDrawnMod); - PostSettingsPanelDraw = IpcSubscribers.PostSettingsPanelDraw.Subscriber(pi, UpdateLastDrawnMod); + PostSettingsPanelDraw = IpcSubscribers.PostSettingsDraw.Subscriber(pi, UpdateLastDrawnMod); ChangedItemTooltip = IpcSubscribers.ChangedItemTooltip.Subscriber(pi, AddedTooltip); ChangedItemClicked = IpcSubscribers.ChangedItemClicked.Subscriber(pi, AddedClick); PreSettingsTabBar.Disable(); @@ -76,7 +76,7 @@ public class UiIpcTester : IUiService, IDisposable if (!table) return; - IpcTester.DrawIntro(IpcSubscribers.PostSettingsPanelDraw.Label, "Last Drawn Mod"); + IpcTester.DrawIntro(IpcSubscribers.PostSettingsDraw.Label, "Last Drawn Mod"); ImGui.TextUnformatted(_lastDrawnMod.Length > 0 ? $"{_lastDrawnMod} at {_lastDrawnModTime}" : "None"); IpcTester.DrawIntro(IpcSubscribers.ChangedItemTooltip.Label, "Add Tooltip"); From 792a04337f31f2c53ff562c3d8116821192fe58e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 15:50:09 +0200 Subject: [PATCH 057/865] Add a try-catch when scanning for mods. --- Penumbra/Mods/Manager/ModManager.cs | 36 ++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 40585520..d912e292 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,3 +1,4 @@ +using System.Security.AccessControl; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -311,22 +312,31 @@ public sealed class ModManager : ModStorage, IDisposable /// private void ScanMods() { - var options = new ParallelOptions() + try { - MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), - }; - var queue = new ConcurrentQueue(); - Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => - { - var mod = Creator.LoadMod(dir, false); - if (mod != null) - queue.Enqueue(mod); - }); + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2), + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Creator.LoadMod(dir, false); + if (mod != null) + queue.Enqueue(mod); + }); - foreach (var mod in queue) + foreach (var mod in queue) + { + mod.Index = Count; + Mods.Add(mod); + } + } + catch (Exception ex) { - mod.Index = Count; - Mods.Add(mod); + Valid = false; + _communicator.ModDirectoryChanged.Invoke(BasePath.FullName, false); + Penumbra.Log.Error($"Could not scan for mods:\n{ex}"); } } } From 07afbfb22974c4ca49d76bce109ff0a301d17b60 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 23 Apr 2024 17:41:55 +0200 Subject: [PATCH 058/865] Rework options, pre-submod types. --- Penumbra/Api/Api/ModSettingsApi.cs | 10 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Meta/MetaFileManager.cs | 8 +- Penumbra/Mods/Editor/ModEditor.cs | 30 +++- Penumbra/Mods/Editor/ModFileEditor.cs | 3 +- Penumbra/Mods/Editor/ModMerger.cs | 38 ++++-- Penumbra/Mods/Editor/ModNormalizer.cs | 72 +++++++--- Penumbra/Mods/Manager/ModCacheManager.cs | 10 +- Penumbra/Mods/Manager/ModMigration.cs | 12 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 128 ++++++++---------- Penumbra/Mods/Mod.cs | 9 +- Penumbra/Mods/ModCreator.cs | 45 +++--- Penumbra/Mods/Subclasses/IModDataContainer.cs | 80 +++++++++++ Penumbra/Mods/Subclasses/IModGroup.cs | 14 +- Penumbra/Mods/Subclasses/IModOption.cs | 25 ++++ Penumbra/Mods/Subclasses/ModSettings.cs | 18 +-- Penumbra/Mods/Subclasses/MultiModGroup.cs | 86 ++++++++---- Penumbra/Mods/Subclasses/SingleModGroup.cs | 82 +++++++---- Penumbra/Mods/Subclasses/SubMod.cs | 110 +++++++++++++-- Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 17 ++- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 10 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 20 ++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 50 ++++--- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 39 +++--- 25 files changed, 620 insertions(+), 300 deletions(-) create mode 100644 Penumbra/Mods/Subclasses/IModDataContainer.cs create mode 100644 Penumbra/Mods/Subclasses/IModOption.cs diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 2604a49d..5ed26ce5 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -56,13 +56,13 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var dict = new Dictionary(mod.Groups.Count); foreach (var g in mod.Groups) - dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type)); + dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)); return new AvailableModSettings(dict); } public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => (g.Select(o => o.Name).ToArray(), (int)g.Type)) + ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)) : null; public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, @@ -153,7 +153,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (groupIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + var optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -190,7 +190,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { case SingleModGroup single: { - var optionIdx = optionNames.Count == 0 ? -1 : single.IndexOf(o => o.Name == optionNames[^1]); + var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { foreach (var name in optionNames) { - var optionIdx = multi.IndexOf(o => o.Name == name); + var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index f4b7d47e..7d9388a9 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -203,7 +203,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); + options.Insert(idx, SubMod.CreateForSaving(option.Name)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 5283f77e..b1823bd7 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -54,7 +54,13 @@ public unsafe class MetaFileManager if (!dir.Exists) dir.Create(); - foreach (var option in group.OfType()) + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + foreach (var option in optionEnumerator) { var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index d9781c06..0a96e0fd 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,3 +1,4 @@ +using System; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; @@ -72,12 +73,18 @@ public class ModEditor( if (groupIdx >= 0) { Group = Mod.Groups[groupIdx]; - if (optionIdx >= 0 && optionIdx < Group.Count) + switch(Group) { - Option = Group[optionIdx]; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; + case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count: + Option = single.OptionData[optionIdx]; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; + case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count: + Option = multi.PrioritizedOptions[optionIdx].Mod; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; } } } @@ -109,8 +116,17 @@ public class ModEditor( action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) { - for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) - action(group[optionIdx], groupIdx, optionIdx); + switch (group) + { + case SingleModGroup single: + for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) + action(single.OptionData[optionIdx], groupIdx, optionIdx); + break; + case MultiModGroup multi: + for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx); + break; + } } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 4bdf4b1b..51615b05 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -24,7 +24,8 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + var (groupIdx, optionIdx) = option.GetIndices(); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); files.UpdatePaths(mod, option); Changes = false; return num; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 25590c49..74e9007c 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; +using ImGuizmoNET; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -104,9 +105,16 @@ public class ModMerger : IDisposable ((List)Warnings).Add( $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); - foreach (var originalOption in originalGroup) + var optionEnumerator = group switch { - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + + foreach (var originalOption in optionEnumerator) + { + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); if (optionCreated) { _createdOptions.Add(option); @@ -138,7 +146,7 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) _createdOptions.Add(option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); @@ -184,9 +192,10 @@ public class ModMerger : IDisposable } } - _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips, SaveType.ImmediateSync); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -251,7 +260,7 @@ public class ModMerger : IDisposable Mod? result = null; try { - dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].ParentMod.Name}."); + dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].Mod.Name}."); if (dir == null) throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); @@ -268,7 +277,6 @@ public class ModMerger : IDisposable { foreach (var originalOption in mods) { - var originalGroup = originalOption.ParentMod.Groups[originalOption.GroupIdx]; if (originalOption.IsDefault) { var files = CopySubModFiles(mods[0], dir); @@ -278,13 +286,14 @@ public class ModMerger : IDisposable } else { + var originalGroup = originalOption.Group; var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); var folder = Path.Combine(dir.FullName, group.Name, option.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.OptionSetFiles(result, groupIdx, option.OptionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, option.OptionIdx, originalOption.FileSwapData); - _editor.OptionSetManipulations(result, groupIdx, option.OptionIdx, originalOption.ManipulationData); + _editor.OptionSetFiles(result, groupIdx, optionIdx, files); + _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); + _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); } } } @@ -309,7 +318,7 @@ public class ModMerger : IDisposable private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) { var ret = new Dictionary(option.FileData.Count); - var parentPath = ((Mod)option.ParentMod).ModPath.FullName; + var parentPath = ((Mod)option.Mod).ModPath.FullName; foreach (var (path, file) in option.FileData) { var target = Path.GetRelativePath(parentPath, file.FullName); @@ -339,7 +348,8 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - _editor.DeleteOption(MergeToMod!, option.GroupIdx, option.OptionIdx); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index a9a31212..9698fdcb 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -167,28 +167,27 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) // Normalize all other options. foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { - _redirections[groupIdx + 1].EnsureCapacity(group.Count); - for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) - _redirections[groupIdx + 1].Add([]); - var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); - foreach (var option in group.OfType()) + switch (group) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + case SingleModGroup single: + _redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count); + for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i) + _redirections[groupIdx + 1].Add([]); - newDict = _redirections[groupIdx + 1][option.OptionIdx]; - newDict.Clear(); - newDict.EnsureCapacity(option.FileData.Count); - foreach (var (gamePath, fullPath) in option.FileData) - { - var relPath = new Utf8RelPath(gamePath).ToString(); - var newFullPath = Path.Combine(optionDir.FullName, relPath); - var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); - Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); - File.Copy(fullPath.FullName, newFullPath, true); - newDict.Add(gamePath, redirectPath); - ++Step; - } + foreach (var (option, optionIdx) in single.OptionData.WithIndex()) + HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); + + break; + case MultiModGroup multi: + _redirections[groupIdx + 1].EnsureCapacity(multi.PrioritizedOptions.Count); + for (var i = _redirections[groupIdx + 1].Count; i < multi.PrioritizedOptions.Count; ++i) + _redirections[groupIdx + 1].Add([]); + + foreach (var ((option, _), optionIdx) in multi.PrioritizedOptions.WithIndex()) + HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); + + break; } } @@ -200,6 +199,24 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) } return false; + + void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary newDict) + { + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + + newDict.Clear(); + newDict.EnsureCapacity(option.FileData.Count); + foreach (var (gamePath, fullPath) in option.FileData) + { + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFullPath = Path.Combine(optionDir.FullName, relPath); + var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); + Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); + File.Copy(fullPath.FullName, newFullPath, true); + newDict.Add(gamePath, redirectPath); + ++Step; + } + } } private bool MoveOldFiles() @@ -274,9 +291,20 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) private void ApplyRedirections() { - foreach (var option in Mod.AllSubMods) - _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, - _redirections[option.GroupIdx + 1][option.OptionIdx]); + foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) + { + switch (group) + { + case SingleModGroup single: + foreach (var (_, optionIdx) in single.OptionData.WithIndex()) + _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + break; + case MultiModGroup multi: + foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) + _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + break; + } + } ++Step; } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 99ad1a4f..df243781 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,6 +2,7 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -211,7 +212,14 @@ public class ModCacheManager : IDisposable foreach (var group in mod.Groups) { mod.HasOptions |= group.IsOption; - foreach (var s in group) + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + + foreach (var s in optionEnumerator) { mod.TotalFileCount += s.Files.Count; mod.TotalSwapCount += s.FileSwaps.Count; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 295afd7b..9c8ced89 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -126,7 +126,7 @@ public static partial class ModMigration case GroupType.Multi: var optionPriority = ModPriority.Default; - var newMultiGroup = new MultiModGroup() + var newMultiGroup = new MultiModGroup(mod) { Name = group.GroupName, Priority = priority++, @@ -134,7 +134,7 @@ public static partial class ModMigration }; mod.Groups.Add(newMultiGroup); foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++)); + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, newMultiGroup, option, seenMetaFiles), optionPriority++)); break; case GroupType.Single: @@ -144,7 +144,7 @@ public static partial class ModMigration return; } - var newSingleGroup = new SingleModGroup() + var newSingleGroup = new SingleModGroup(mod) { Name = group.GroupName, Priority = priority++, @@ -152,7 +152,7 @@ public static partial class ModMigration }; mod.Groups.Add(newSingleGroup); foreach (var option in group.Options) - newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles)); + newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, newSingleGroup, option, seenMetaFiles)); break; } @@ -171,9 +171,9 @@ public static partial class ModMigration } } - private static SubMod SubModFromOption(ModCreator creator, Mod mod, OptionV0 option, HashSet seenMetaFiles) + private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SubMod(mod) { Name = option.OptionName }; + var subMod = new SubMod(mod, group) { Name = option.OptionName }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 9d942574..e78b6209 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -87,12 +87,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; mod.Groups.Add(type == GroupType.Multi - ? new MultiModGroup + ? new MultiModGroup(mod) { Name = newName, Priority = maxPriority, } - : new SingleModGroup + : new SingleModGroup(mod) { Name = newName, Priority = maxPriority, @@ -120,7 +120,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod.Groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); } @@ -131,7 +130,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) return; - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } @@ -156,12 +154,9 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Change the description of the given option. public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - var group = mod.Groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription) + if (!mod.Groups[groupIdx].ChangeOptionDescription(optionIdx, newDescription)) return; - option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -173,12 +168,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (group.Priority == newPriority) return; - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; + group.Priority = newPriority; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } @@ -188,14 +178,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { switch (mod.Groups[groupIdx]) { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + case MultiModGroup multi: + if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) return; - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; @@ -205,60 +192,63 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Rename the given option. public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - switch (mod.Groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } + if (!mod.Groups[groupIdx].ChangeOptionName(optionIdx, newName)) + return; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } /// Add a new empty option of the given name for the given group. - public void AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, ModPriority.Default)); - break; - } + var group = mod.Groups[groupIdx]; + var idx = group.AddOption(mod, newName); + if (idx < 0) + return -1; saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return idx; } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; - var idx = group.IndexOf(o => o.Name == newName); - if (idx >= 0) - return ((SubMod)group[idx], false); + switch (group) + { + case SingleModGroup single: + { + var idx = single.OptionData.IndexOf(o => o.Name == newName); + if (idx >= 0) + return (single.OptionData[idx], idx, false); - AddOption(mod, groupIdx, newName, saveType); - if (group[^1].Name != newName) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + idx = single.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - return ((SubMod)group[^1], true); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (single.OptionData[^1], single.OptionData.Count - 1, true); + } + case MultiModGroup multi: + { + var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName); + if (idx >= 0) + return (multi.PrioritizedOptions[idx].Mod, idx, false); + + idx = multi.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true); + } + } + + throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}."); } /// Add an existing option to a given group with default priority. @@ -269,25 +259,28 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) { var group = mod.Groups[groupIdx]; + int idx; switch (group) { - case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }: + case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: - option.SetPosition(groupIdx, s.Count); + idx = s.OptionData.Count; s.OptionData.Add(option); break; case MultiModGroup m: - option.SetPosition(groupIdx, m.Count); + idx = m.PrioritizedOptions.Count; m.PrioritizedOptions.Add((option, priority)); break; + default: + return; } saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); } /// Delete the given option from the given group. @@ -306,7 +299,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; } - group.UpdatePositions(optionIdx); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } @@ -396,16 +388,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return false; } - /// Update the indices stored in options from a given group on. - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup)) - { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } - /// Get the correct option for the given group and option index. private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) { diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 25f3c510..71f64205 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -38,7 +38,7 @@ public sealed class Mod : IMod internal Mod(DirectoryInfo modPath) { ModPath = modPath; - Default = new SubMod(this); + Default = SubMod.CreateDefault(this); } public override string ToString() @@ -82,7 +82,12 @@ public sealed class Mod : IMod } public IEnumerable AllSubMods - => Groups.SelectMany(o => o).Prepend(Default); + => Groups.SelectMany(o => o switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod), + _ => [], + }).Prepend(Default); public List FindUnusedFiles() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 661dd6fb..4d32f395 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -16,7 +16,11 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class ModCreator(SaveService _saveService, Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, +public partial class ModCreator( + SaveService _saveService, + Configuration config, + ModDataEditor _dataEditor, + MetaFileManager _metaFileManager, GamePathParser _gamePathParser) { public readonly Configuration Config = config; @@ -106,7 +110,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config, public void LoadDefaultOption(Mod mod) { var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); - mod.Default.SetPosition(-1, 0); try { if (!File.Exists(defaultFile)) @@ -241,27 +244,21 @@ public partial class ModCreator(SaveService _saveService, Configuration config, { case GroupType.Multi: { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; + var group = MultiModGroup.CreateForSaving(name); + group.Description = desc; + group.Priority = priority; + group.DefaultSettings = defaultSettings; group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange(subMods.OfType()); + var group = SingleModGroup.CreateForSaving(name); + group.Description = desc; + group.Priority = priority; + group.DefaultSettings = defaultSettings; + group.OptionData.AddRange(subMods); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -275,11 +272,8 @@ public partial class ModCreator(SaveService _saveService, Configuration config, .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; + var mod = SubMod.CreateForSaving(option.Name); + mod.Description = option.Description; foreach (var (_, gamePath, file) in list) mod.FileData.TryAdd(gamePath, file); @@ -287,13 +281,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config, return mod; } - /// Create an empty sub mod for single groups with None options. - internal static SubMod CreateEmptySubMod(string name) - => new SubMod(null!) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - /// /// Create the default data file from all unused files that were not handled before /// and are used in sub mods. diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/Subclasses/IModDataContainer.cs new file mode 100644 index 00000000..d0b444b8 --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModDataContainer.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Subclasses; + +public interface IModDataContainer +{ + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } + + public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) + { + data.Files.Clear(); + data.FileSwaps.Clear(); + data.Manipulations.Clear(); + + var files = (JObject?)json[nameof(Files)]; + if (files != null) + foreach (var property in files.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); + } + + var swaps = (JObject?)json[nameof(FileSwaps)]; + if (swaps != null) + foreach (var property in swaps.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); + } + + var manips = json[nameof(Manipulations)]; + if (manips != null) + foreach (var s in manips.Children().Select(c => c.ToObject()) + .Where(m => m.Validate())) + data.Manipulations.Add(s); + } + + public static void WriteModData(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) + { + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 96d7c6b7..a046ade0 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -6,25 +6,27 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IReadOnlyCollection +public interface IModGroup { public const int MaxMultiOptions = 63; + public Mod Mod { get; } public string Name { get; } public string Description { get; } public GroupType Type { get; } - public ModPriority Priority { get; } + public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); + public int AddOption(Mod mod, string name, string description = ""); + public bool ChangeOptionDescription(int optionIndex, string newDescription); + public bool ChangeOptionName(int optionIndex, string newName); - public SubMod this[Index idx] { get; } - - public bool IsOption { get; } + public IReadOnlyList Options { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); - public void UpdatePositions(int from = 0); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/Subclasses/IModOption.cs new file mode 100644 index 00000000..bb52a2cd --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModOption.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.Mods.Subclasses; + +public interface IModOption +{ + public string Name { get; set; } + public string FullName { get; } + public string Description { get; set; } + + public static void Load(JToken json, IModOption option) + { + option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; + } + + public static void WriteModOption(JsonWriter j, IModOption option) + { + j.WritePropertyName(nameof(Name)); + j.WriteValue(option.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(option.Description); + } +} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 81a3bb41..2ddabdb8 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -68,7 +68,7 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config.TurnMulti(group.Count), + GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; @@ -182,15 +182,15 @@ public class ModSettings if (idx >= mod.Groups.Count) break; - var group = mod.Groups[idx]; - if (group.Type == GroupType.Single && setting.Value < (ulong)group.Count) + switch (mod.Groups[idx]) { - dict.Add(group.Name, [group[(int)setting.Value].Name]); - } - else - { - var list = group.Where((_, optionIdx) => (setting.Value & (1ul << optionIdx)) != 0).Select(o => o.Name).ToList(); - dict.Add(group.Name, list); + case SingleModGroup single when setting.Value < (ulong)single.Options.Count: + dict.Add(single.Name, [single.Options[setting.AsIndex].Name]); + break; + case MultiModGroup multi: + var list = multi.Options.WithIndex().Where(p => setting.HasFlag(p.Index)).Select(p => p.Value.Name).ToList(); + dict.Add(multi.Name, list); + break; } } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 02ae07f4..4ec2c72a 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,5 +1,4 @@ using Dalamud.Interface.Internal.Notifications; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -11,11 +10,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow all available options to be selected at once. -public sealed class MultiModGroup : IModGroup +public sealed class MultiModGroup(Mod mod) : IModGroup { public GroupType Type => GroupType.Multi; + public Mod Mod { get; set; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; public ModPriority Priority { get; set; } @@ -26,27 +26,58 @@ public sealed class MultiModGroup : IModGroup .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public SubMod this[Index idx] - => PrioritizedOptions[idx].Mod; + public int AddOption(Mod mod, string name, string description = "") + { + var groupIdx = mod.Groups.IndexOf(this); + if (groupIdx < 0) + return -1; + + var subMod = new SubMod(mod, this) + { + Name = name, + Description = description, + }; + PrioritizedOptions.Add((subMod, ModPriority.Default)); + return PrioritizedOptions.Count - 1; + } + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) + return false; + + var option = PrioritizedOptions[optionIndex].Mod; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) + return false; + + var option = PrioritizedOptions[optionIndex].Mod; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public IReadOnlyList Options + => PrioritizedOptions.Select(p => p.Mod).ToArray(); public bool IsOption - => Count > 0; - - [JsonIgnore] - public int Count - => PrioritizedOptions.Count; + => PrioritizedOptions.Count > 0; public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public IEnumerator GetEnumerator() - => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { - var ret = new MultiModGroup() + var ret = new MultiModGroup(mod) { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, @@ -68,8 +99,7 @@ public sealed class MultiModGroup : IModGroup break; } - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + var subMod = new SubMod(mod, ret); subMod.Load(mod.ModPath, child, out var priority); ret.PrioritizedOptions.Add((subMod, priority)); } @@ -85,12 +115,12 @@ public sealed class MultiModGroup : IModGroup { case GroupType.Multi: return this; case GroupType.Single: - var multi = new SingleModGroup() + var multi = new SingleModGroup(Mod) { Name = Name, Description = Description, Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(Count), + DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), }; multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; @@ -104,16 +134,9 @@ public sealed class MultiModGroup : IModGroup return false; DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions(int from = 0) - { - foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); - } - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) @@ -124,5 +147,12 @@ public sealed class MultiModGroup : IModGroup } public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << Count) - 1)); + => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); + + /// Create a group without a mod only for saving it in the creator. + internal static MultiModGroup CreateForSaving(string name) + => new(null!) + { + Name = name, + }; } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index b854d2b1..994a1f96 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; @@ -9,11 +8,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. -public sealed class SingleModGroup : IModGroup +public sealed class SingleModGroup(Mod mod) : IModGroup { public GroupType Type => GroupType.Single; + public Mod Mod { get; set; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public ModPriority Priority { get; set; } @@ -26,26 +26,53 @@ public sealed class SingleModGroup : IModGroup .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public SubMod this[Index idx] - => OptionData[idx]; + public int AddOption(Mod mod, string name, string description = "") + { + var subMod = new SubMod(mod, this) + { + Name = name, + Description = description, + }; + OptionData.Add(subMod); + return OptionData.Count - 1; + } + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= OptionData.Count) + return false; + + var option = OptionData[optionIndex]; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= OptionData.Count) + return false; + + var option = OptionData[optionIndex]; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public IReadOnlyList Options + => OptionData; public bool IsOption - => Count > 1; - - [JsonIgnore] - public int Count - => OptionData.Count; - - public IEnumerator GetEnumerator() - => OptionData.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); + => OptionData.Count > 1; public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) { var options = json["Options"]; - var ret = new SingleModGroup + var ret = new SingleModGroup(mod) { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, @@ -58,8 +85,7 @@ public sealed class SingleModGroup : IModGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.OptionData.Count); + var subMod = new SubMod(mod, ret); subMod.Load(mod.ModPath, child, out _); ret.OptionData.Add(subMod); } @@ -74,7 +100,7 @@ public sealed class SingleModGroup : IModGroup { case GroupType.Single: return this; case GroupType.Multi: - var multi = new MultiModGroup() + var multi = new MultiModGroup(Mod) { Name = Name, Description = Description, @@ -108,19 +134,19 @@ public sealed class SingleModGroup : IModGroup DefaultSettings = Setting.Single(currentIndex + 1); } - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions(int from = 0) - { - foreach (var (o, i) in OptionData.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); - } - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => this[setting.AsIndex].AddData(redirections, manipulations); + => OptionData[setting.AsIndex].AddData(redirections, manipulations); public Setting FixSetting(Setting setting) - => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); + => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); + + /// Create a group without a mod only for saving it in the creator. + internal static SingleModGroup CreateForSaving(string name) + => new(null!) + { + Name = name, + }; } diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 386910e5..bc93fcc4 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,11 +1,62 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; +public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer +{ + internal readonly Mod Mod = mod; + internal readonly SingleModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + +public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer +{ + internal readonly Mod Mod = mod; + internal readonly MultiModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + public ModPriority Priority { get; set; } = ModPriority.Default; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + +public class DefaultSubMod(IMod mod) : IModDataContainer +{ + public string FullName + => "Default Option"; + + public string Description + => string.Empty; + + internal readonly IMod Mod = mod; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + /// /// A sub mod is a collection of /// - file replacements @@ -16,21 +67,51 @@ namespace Penumbra.Mods.Subclasses; /// Nothing is checked for existence or validity when loading. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// -public sealed class SubMod +public sealed class SubMod(IMod mod, IModGroup group) : IModOption { public string Name { get; set; } = "Default"; public string FullName - => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}"; + => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; public string Description { get; set; } = string.Empty; - internal IMod ParentMod { get; private init; } - internal int GroupIdx { get; private set; } - internal int OptionIdx { get; private set; } + internal readonly IMod Mod = mod; + internal readonly IModGroup? Group = group; + internal (int GroupIdx, int OptionIdx) GetIndices() + { + if (IsDefault) + return (-1, 0); + + var groupIdx = Mod.Groups.IndexOf(Group); + if (groupIdx < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); + + return (groupIdx, GetOptionIndex()); + } + + private int GetOptionIndex() + { + var optionIndex = Group switch + { + null => 0, + SingleModGroup single => single.OptionData.IndexOf(this), + MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this), + _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), + }; + if (optionIndex < 0) + throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); + + return optionIndex; + } + + public static SubMod CreateDefault(IMod mod) + => new(mod, null!); + + [MemberNotNullWhen(false, nameof(Group))] public bool IsDefault - => GroupIdx < 0; + => Group == null; public void AddData(Dictionary redirections, HashSet manipulations) { @@ -46,9 +127,6 @@ public sealed class SubMod public Dictionary FileSwapData = []; public HashSet ManipulationData = []; - public SubMod(IMod parentMod) - => ParentMod = parentMod; - public IReadOnlyDictionary Files => FileData; @@ -58,12 +136,6 @@ public sealed class SubMod public IReadOnlySet Manipulations => ManipulationData; - public void SetPosition(int groupIdx, int optionIdx) - { - GroupIdx = groupIdx; - OptionIdx = optionIdx; - } - public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) { FileData.Clear(); @@ -116,6 +188,14 @@ public sealed class SubMod } } + /// Create a sub mod without a mod or group only for saving it in the creator. + internal static SubMod CreateForSaving(string name) + => new(null!, null!) + { + Name = name, + }; + + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) { j.WriteStartObject(); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 41c1211f..a599b3bb 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -49,7 +49,7 @@ public class TemporaryMod : IMod => [Default]; public TemporaryMod() - => Default = new SubMod(this); + => Default = SubMod.CreateDefault(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) => Default.FileData[gamePath] = fullPath; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 7b5ce2dc..5125a5b2 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -253,7 +253,7 @@ public class ItemSwapTab : IDisposable, ITab _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 - && (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true); + && (_selectedGroup?.Options.All(o => o.Name != _newOptionName) ?? true); } private void CreateMod() @@ -275,7 +275,7 @@ public class ItemSwapTab : IDisposable, ITab var groupCreated = false; var dirCreated = false; - var optionCreated = false; + var optionCreated = -1; DirectoryInfo? optionFolderName = null; try { @@ -294,14 +294,17 @@ public class ItemSwapTab : IDisposable, ITab groupCreated = true; } - _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); - optionCreated = true; + var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); + if (optionIdx < 0) + throw new Exception($"Failure creating mod option."); + + optionCreated = optionIdx; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, - _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) + _mod.Groups.IndexOf(_selectedGroup), optionIdx)) throw new Exception("Failure writing files for mod swap."); } } @@ -310,8 +313,8 @@ public class ItemSwapTab : IDisposable, ITab Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { - if (optionCreated && _selectedGroup != null) - _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); + if (optionCreated >= 0 && _selectedGroup != null) + _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); if (groupCreated) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a70da628..3f5f6c37 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -78,7 +78,10 @@ public partial class ModEditWindow : Window, IDisposable } public void ChangeOption(SubMod? subMod) - => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.OptionIdx ?? 0); + { + var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, optionIdx); + } public void UpdateModels() { @@ -428,7 +431,8 @@ public partial class ModEditWindow : Window, IDisposable using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) { - _editor.LoadOption(option.GroupIdx, option.OptionIdx); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.LoadOption(groupIdx, optionIdx); ret = true; } } @@ -565,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable } if (Mod != null) - foreach (var option in Mod.Groups.SelectMany(g => g).Append(Mod.Default)) + foreach (var option in Mod.AllSubMods) { foreach (var path in option.Files.Keys) { diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 1df814da..c34c7ef0 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -71,7 +71,7 @@ public class ModMergeTab(ModMerger modMerger) color = color == Colors.DiscordColor ? Colors.DiscordColor - : group == null || group.Any(o => o.Name == modMerger.OptionName) + : group == null || group.Options.Any(o => o.Name == modMerger.OptionName) ? Colors.PressEnterWarningBg : Colors.DiscordColor; c.Push(ImGuiCol.Border, color); @@ -184,18 +184,26 @@ public class ModMergeTab(ModMerger modMerger) else { ImGuiUtil.DrawTableColumn(option.Name); - var group = option.ParentMod.Groups[option.GroupIdx]; + var group = option.Group; + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) { if (ImGui.MenuItem("Select All")) - foreach (var opt in group) - Handle((SubMod)opt, true); + // ReSharper disable once PossibleMultipleEnumeration + foreach (var opt in optionEnumerator) + Handle(opt, true); if (ImGui.MenuItem("Unselect All")) - foreach (var opt in group) - Handle((SubMod)opt, false); + // ReSharper disable once PossibleMultipleEnumeration + foreach (var opt in optionEnumerator) + Handle(opt, false); ImGui.EndPopup(); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index b002dedd..0dc694d8 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -324,7 +324,7 @@ public class ModPanelEditTab( ? mod.Description : optionIdx < 0 ? mod.Groups[groupIdx].Description - : mod.Groups[groupIdx][optionIdx].Description; + : mod.Groups[groupIdx].Options[optionIdx].Description; _oldDescription = _newDescription; _mod = mod; @@ -479,17 +479,24 @@ public class ModPanelEditTab( ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); - var group = panel._mod.Groups[groupIdx]; - for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) - EditOption(panel, group, groupIdx, optionIdx); - + switch (panel._mod.Groups[groupIdx]) + { + case SingleModGroup single: + for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) + EditOption(panel, single, groupIdx, optionIdx); + break; + case MultiModGroup multi: + for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + EditOption(panel, multi, groupIdx, optionIdx); + break; + } DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); } /// Draw a line for a single option. private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) { - var option = group[optionIdx]; + var option = group.Options[optionIdx]; using var id = ImRaii.PushId(optionIdx); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -547,10 +554,16 @@ public class ModPanelEditTab( { var mod = panel._mod; var group = mod.Groups[groupIdx]; + var count = group switch + { + SingleModGroup single => single.OptionData.Count, + MultiModGroup multi => multi.PrioritizedOptions.Count, + _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), + }; ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{group.Count + 1}"); - Target(panel, group, groupIdx, group.Count); + ImGui.Selectable($"Option #{count + 1}"); + Target(panel, group, groupIdx, count); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); @@ -562,7 +575,7 @@ public class ModPanelEditTab( } ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || mod.Groups[groupIdx].Count < IModGroup.MaxMultiOptions; + var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions; var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var tt = canAddGroup ? validName ? "Add a new option to this group." : "Please enter a name for the new option." @@ -588,7 +601,7 @@ public class ModPanelEditTab( _dragDropOptionIdx = optionIdx; } - ImGui.TextUnformatted($"Dragging option {group[optionIdx].Name} from group {group.Name}..."); + ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}..."); } private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) @@ -611,12 +624,17 @@ public class ModPanelEditTab( var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group.Count; - var option = sourceGroup[sourceOption]; - var priority = sourceGroup switch + var currentCount = group switch { - MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, - _ => ModPriority.Default, + SingleModGroup single => single.OptionData.Count, + MultiModGroup multi => multi.PrioritizedOptions.Count, + _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), + }; + var (option, priority) = sourceGroup switch + { + SingleModGroup single => (single.OptionData[_dragDropOptionIdx], ModPriority.Default), + MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx], + _ => throw new Exception($"Dragging options from an option group of type {sourceGroup.GetType()} is not supported."), }; panel._delayedActions.Enqueue(() => { @@ -651,7 +669,7 @@ public class ModPanelEditTab( if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); - var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; + var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1107aa20..cb76088c 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -75,7 +75,7 @@ public class ModPanelSettingsTab : ITab { var useDummy = true; foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() - .Where(g => g.Value.Type == GroupType.Single && g.Value.Count > _config.SingleGroupRadioMax)) + .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax)) { ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; @@ -92,7 +92,7 @@ public class ModPanelSettingsTab : ITab case GroupType.Multi: DrawMultiGroup(group, idx); break; - case GroupType.Single when group.Count <= _config.SingleGroupRadioMax: + case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax: DrawSingleGroupRadio(group, idx); break; } @@ -181,13 +181,14 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) + var options = group.Options; + using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) { if (combo) - for (var idx2 = 0; idx2 < group.Count; ++idx2) + for (var idx2 = 0; idx2 < options.Count; ++idx2) { id.Push(idx2); - var option = group[idx2]; + var option = options[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Single(idx2)); @@ -213,18 +214,18 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - - DrawCollapseHandling(group, minWidth, DrawOptions); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); return; void DrawOptions() { - for (var idx = 0; idx < group.Count; ++idx) + for (var idx = 0; idx < group.Options.Count; ++idx) { using var i = ImRaii.PushId(idx); - var option = group[idx]; + var option = options[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Single(idx)); @@ -239,9 +240,9 @@ public class ModPanelSettingsTab : ITab } - private void DrawCollapseHandling(IModGroup group, float minWidth, Action draw) + private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) { - if (group.Count <= _config.OptionGroupCollapsibleMin) + if (options.Count <= _config.OptionGroupCollapsibleMin) { draw(); } @@ -249,8 +250,8 @@ public class ModPanelSettingsTab : ITab { var collapseId = ImGui.GetID("Collapse"); var shown = ImGui.GetStateStorage().GetBool(collapseId, true); - var buttonTextShow = $"Show {group.Count} Options"; - var buttonTextHide = $"Hide {group.Count} Options"; + var buttonTextShow = $"Show {options.Count} Options"; + var buttonTextHide = $"Hide {options.Count} Options"; var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); @@ -274,7 +275,7 @@ public class ModPanelSettingsTab : ITab } else { - var optionWidth = group.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; @@ -294,8 +295,8 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - - DrawCollapseHandling(group, minWidth, DrawOptions); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; @@ -307,10 +308,10 @@ public class ModPanelSettingsTab : ITab void DrawOptions() { - for (var idx = 0; idx < group.Count; ++idx) + for (var idx = 0; idx < options.Count; ++idx) { using var i = ImRaii.PushId(idx); - var option = group[idx]; + var option = options[idx]; var setting = flags.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref setting)) @@ -339,7 +340,7 @@ public class ModPanelSettingsTab : ITab ImGui.Separator(); if (ImGui.Selectable("Enable All")) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.AllBits(group.Count)); + Setting.AllBits(group.Options.Count)); if (ImGui.Selectable("Disable All")) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); From 6b1743b776da300fbe1b80a16e662b66ea519a42 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:04:04 +0200 Subject: [PATCH 059/865] This sucks so hard... --- Penumbra/Api/Api/ModSettingsApi.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 6 +- Penumbra/Meta/MetaFileManager.cs | 14 +- Penumbra/Mods/Editor/DuplicateManager.cs | 6 +- Penumbra/Mods/Editor/FileRegistry.cs | 12 +- Penumbra/Mods/Editor/ModEditor.cs | 63 +-- Penumbra/Mods/Editor/ModFileCollection.cs | 16 +- Penumbra/Mods/Editor/ModFileEditor.cs | 22 +- Penumbra/Mods/Editor/ModMerger.cs | 79 ++- Penumbra/Mods/Editor/ModMetaEditor.cs | 4 +- Penumbra/Mods/Editor/ModNormalizer.cs | 38 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 29 +- Penumbra/Mods/Manager/ModMigration.cs | 39 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 144 +++--- Penumbra/Mods/Mod.cs | 19 +- Penumbra/Mods/ModCreator.cs | 41 +- Penumbra/Mods/Subclasses/IModDataContainer.cs | 48 ++ Penumbra/Mods/Subclasses/IModGroup.cs | 126 +++-- Penumbra/Mods/Subclasses/IModOption.cs | 2 + Penumbra/Mods/Subclasses/MultiModGroup.cs | 122 ++--- Penumbra/Mods/Subclasses/SingleModGroup.cs | 75 +-- Penumbra/Mods/Subclasses/SubMod.cs | 477 +++++++++++------- Penumbra/Mods/TemporaryMod.cs | 35 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 15 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- .../ModEditWindow.Models.MdlTab.cs | 4 +- .../ModEditWindow.QuickImport.cs | 12 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 28 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 33 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 28 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- 33 files changed, 852 insertions(+), 695 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 5ed26ce5..e145e027 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { foreach (var name in optionNames) { - var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); + var optionIdx = multi.OptionData.IndexOf(o => o.Mod.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7d9388a9..b9cdda71 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -152,7 +152,7 @@ public partial class TexToolsImporter } // Iterate through all pages - var options = new List(); + var options = new List(); var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) @@ -183,7 +183,7 @@ public partial class TexToolsImporter var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); ExtractSimpleModList(optionFolder, option.ModsJsons); - options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); + options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option, new ModPriority(i))); if (option.IsChecked) defaultSettings = group.SelectionType == GroupType.Multi ? defaultSettings!.Value | Setting.Multi(i) @@ -203,7 +203,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, SubMod.CreateForSaving(option.Name)); + options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index b1823bd7..0e2e638b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -50,17 +50,15 @@ public unsafe class MetaFileManager TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); foreach (var group in mod.Groups) { + if (group is not ITexToolsGroup texToolsGroup) + continue; + var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport); if (!dir.Exists) dir.Create(); - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - foreach (var option in optionEnumerator) + + foreach (var option in texToolsGroup.OptionData) { var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) @@ -99,7 +97,7 @@ public unsafe class MetaFileManager return; ResidentResources.Reload(); - if (collection?._cache == null) + if (collection._cache == null) CharacterUtility.ResetAll(); else collection._cache.Meta.SetFiles(); diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 938199aa..92ec58b9 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } - public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, IModDataContainer option, bool useModManager) { if (!Worker.IsCompleted || _duplicates.Count == 0) return; @@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -86,7 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } else { - subMod.FileData = dict; + subMod.Files = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 427c58ca..44d349ce 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -5,12 +5,12 @@ namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; - public FullPath File { get; private init; } - public Utf8RelPath RelPath { get; private init; } - public long FileSize { get; private init; } - public int CurrentUsage; - public bool IsOnPlayer; + public readonly List<(IModDataContainer, Utf8GamePath)> SubModUsage = []; + public FullPath File { get; private init; } + public Utf8RelPath RelPath { get; private init; } + public long FileSize { get; private init; } + public int CurrentUsage; + public bool IsOnPlayer; public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry) { diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 0a96e0fd..1118f890 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,4 +1,3 @@ -using System; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; @@ -25,20 +24,20 @@ public class ModEditor( public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; public readonly FileCompactor Compactor = compactor; - public Mod? Mod { get; private set; } - public int GroupIdx { get; private set; } - public int OptionIdx { get; private set; } + public Mod? Mod { get; private set; } + public int GroupIdx { get; private set; } + public int DataIdx { get; private set; } - public IModGroup? Group { get; private set; } - public SubMod? Option { get; private set; } + public IModGroup? Group { get; private set; } + public IModDataContainer? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); - public void LoadMod(Mod mod, int groupIdx, int optionIdx) + public void LoadMod(Mod mod, int groupIdx, int dataIdx) { Mod = mod; - LoadOption(groupIdx, optionIdx, true); + LoadOption(groupIdx, dataIdx, true); Files.UpdateAll(mod, Option!); SwapEditor.Revert(Option!); MetaEditor.Load(Mod!, Option!); @@ -46,9 +45,9 @@ public class ModEditor( MdlMaterialEditor.ScanModels(Mod!); } - public void LoadOption(int groupIdx, int optionIdx) + public void LoadOption(int groupIdx, int dataIdx) { - LoadOption(groupIdx, optionIdx, true); + LoadOption(groupIdx, dataIdx, true); SwapEditor.Revert(Option!); Files.UpdatePaths(Mod!, Option!); MetaEditor.Load(Mod!, Option!); @@ -57,44 +56,38 @@ public class ModEditor( } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. - private void LoadOption(int groupIdx, int optionIdx, bool message) + private void LoadOption(int groupIdx, int dataIdx, bool message) { if (Mod != null && Mod.Groups.Count > groupIdx) { - if (groupIdx == -1 && optionIdx == 0) + if (groupIdx == -1 && dataIdx == 0) { - Group = null; - Option = Mod.Default; - GroupIdx = groupIdx; - OptionIdx = optionIdx; + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } if (groupIdx >= 0) { Group = Mod.Groups[groupIdx]; - switch(Group) + if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count) { - case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count: - Option = single.OptionData[optionIdx]; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; - case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count: - Option = multi.PrioritizedOptions[optionIdx].Mod; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; + Option = Group.DataContainers[dataIdx]; + GroupIdx = groupIdx; + DataIdx = dataIdx; + return; } } } - Group = null; - Option = Mod?.Default; - GroupIdx = -1; - OptionIdx = 0; + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + DataIdx = 0; if (message) - Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); + Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } public void Clear() @@ -111,7 +104,7 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllOptions(Mod mod, Action action) { action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) @@ -123,8 +116,8 @@ public class ModEditor( action(single.OptionData[optionIdx], groupIdx, optionIdx); break; case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) - action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx); + for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) + action(multi.OptionData[optionIdx], groupIdx, optionIdx); break; } } diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 9dd78217..ede35914 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public void UpdateAll(Mod mod, SubMod option) + public void UpdateAll(Mod mod, IModDataContainer option) { UpdateFiles(mod, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken()); } - public void UpdatePaths(Mod mod, SubMod option) + public void UpdatePaths(Mod mod, IModDataContainer option) => UpdatePaths(mod, option, true, new CancellationToken()); public void Clear() @@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable public void ClearMissingFiles() => _missing.Clear(); - public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Remove(gamePath); if (file != null) @@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable } } - public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) + public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Add(gamePath); if (file == null) @@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable file.SubModUsage.Add((option, gamePath)); } - public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) + public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) @@ -154,14 +154,14 @@ public class ModFileCollection : IDisposable _usedPaths.Clear(); } - private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) + private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok) { tok.ThrowIfCancellationRequested(); ClearPaths(clearRegistries, tok); tok.ThrowIfCancellationRequested(); - foreach (var subMod in mod.AllSubMods) + foreach (var subMod in mod.AllDataContainers) { foreach (var (gamePath, file) in subMod.Files) { diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 51615b05..11e35334 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -14,7 +14,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu Changes = false; } - public int Apply(Mod mod, SubMod option) + public int Apply(Mod mod, IModDataContainer option) { var dict = new Dictionary(); var num = 0; @@ -24,23 +24,23 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - var (groupIdx, optionIdx) = option.GetIndices(); - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); + var (groupIdx, dataIdx) = option.GetDataIndices(); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict); files.UpdatePaths(mod, option); Changes = false; return num; } - public void Revert(Mod mod, SubMod option) + public void Revert(Mod mod, IModDataContainer option) { files.UpdateAll(mod, option); Changes = false; } /// Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths(Mod mod, SubMod option) + public void RemoveMissingPaths(Mod mod, IModDataContainer option) { - void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -62,7 +62,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// If path is empty, it will be deleted instead. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// - public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + public bool SetGamePath(IModDataContainer option, int fileIdx, int pathIdx, Utf8GamePath path) { if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; @@ -85,7 +85,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. /// - public int AddPathsToSelected(SubMod option, IEnumerable files1, int skipFolders = 0) + public int AddPathsToSelected(IModDataContainer option, IEnumerable files1, int skipFolders = 0) { var failed = 0; foreach (var file in files1) @@ -112,7 +112,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(SubMod option, IEnumerable files1) + public void RemovePathsFromSelected(IModDataContainer option, IEnumerable files1) { foreach (var file in files1) { @@ -130,7 +130,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, SubMod option, IEnumerable files1) + public void DeleteFiles(Mod mod, IModDataContainer option, IEnumerable files1) { var deletions = 0; foreach (var file in files1) @@ -156,7 +156,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } - private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + private bool CheckAgainstMissing(Mod mod, IModDataContainer option, FullPath file, Utf8GamePath key, bool removeUsed) { if (!files.Missing.Contains(file)) return true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 74e9007c..541c84ae 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,6 +1,5 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; -using ImGuizmoNET; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -33,9 +32,9 @@ public class ModMerger : IDisposable private readonly Dictionary _fileToFile = []; private readonly HashSet _createdDirectories = []; private readonly HashSet _createdGroups = []; - private readonly HashSet _createdOptions = []; + private readonly HashSet _createdOptions = []; - public readonly HashSet SelectedOptions = []; + public readonly HashSet SelectedOptions = []; public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } @@ -94,7 +93,7 @@ public class ModMerger : IDisposable private void MergeWithOptions() { - MergeIntoOption(Enumerable.Repeat(MergeFromMod!.Default, 1), MergeToMod!.Default, false); + MergeIntoOption([MergeFromMod!.Default], MergeToMod!.Default, false); foreach (var originalGroup in MergeFromMod!.Groups) { @@ -105,20 +104,13 @@ public class ModMerger : IDisposable ((List)Warnings).Add( $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); - var optionEnumerator = group switch + foreach (var originalOption in group.DataContainers) { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - - foreach (var originalOption in optionEnumerator) - { - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName()); if (optionCreated) { - _createdOptions.Add(option); - MergeIntoOption(Enumerable.Repeat(originalOption, 1), option, false); + _createdOptions.Add((IModDataOption)option); + MergeIntoOption([originalOption], (IModDataOption)option, false); } else { @@ -136,7 +128,7 @@ public class ModMerger : IDisposable if (groupName.Length == 0 && optionName.Length == 0) { CopyFiles(MergeToMod!.ModPath); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default, true); + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), MergeToMod!.Default, true); } else if (groupName.Length * optionName.Length == 0) { @@ -148,7 +140,7 @@ public class ModMerger : IDisposable _createdGroups.Add(groupIdx); var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) - _createdOptions.Add(option); + _createdOptions.Add((IModDataOption)option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -156,14 +148,14 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) + private void MergeIntoOption(IEnumerable mergeOptions, IModDataContainer option, bool fromFileToFile) { - var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var manips = option.ManipulationData.ToHashSet(); + var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var manips = option.Manipulations.ToHashSet(); foreach (var originalOption in mergeOptions) { @@ -171,31 +163,31 @@ public class ModMerger : IDisposable { if (!manips.Add(manip)) throw new Exception( - $"Could not add meta manipulation {manip} from {originalOption.FullName} to {option.FullName} because another manipulation of the same data already exists in this option."); + $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); } foreach (var (swapA, swapB) in originalOption.FileSwaps) { if (!swaps.TryAdd(swapA, swapB)) throw new Exception( - $"Could not add file swap {swapB} -> {swapA} from {originalOption.FullName} to {option.FullName} because another swap of the key already exists."); + $"Could not add file swap {swapB} -> {swapA} from {originalOption.GetFullName()} to {option.GetFullName()} because another swap of the key already exists."); } foreach (var (gamePath, path) in originalOption.Files) { if (!GetFullPath(path, out var newFile)) throw new Exception( - $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because the file does not exist in the new mod."); if (!redirections.TryAdd(gamePath, newFile)) throw new Exception( - $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because a redirection for the game path already exists."); } } - var (groupIdx, optionIdx) = option.GetIndices(); - _editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync); + var (groupIdx, dataIdx) = option.GetDataIndices(); + _editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -270,30 +262,29 @@ public class ModMerger : IDisposable { var files = CopySubModFiles(mods[0], dir); _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); - _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); + _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); } else { foreach (var originalOption in mods) { - if (originalOption.IsDefault) + if (originalOption.Group is not {} originalGroup) { var files = CopySubModFiles(mods[0], dir); _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); - _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); + _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); } else { - var originalGroup = originalOption.Group; - var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group.Name, option.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); _editor.OptionSetFiles(result, groupIdx, optionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); - _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); + _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps); + _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); } } } @@ -315,11 +306,11 @@ public class ModMerger : IDisposable } } - private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) + private static Dictionary CopySubModFiles(IModDataContainer option, DirectoryInfo newMod) { - var ret = new Dictionary(option.FileData.Count); + var ret = new Dictionary(option.Files.Count); var parentPath = ((Mod)option.Mod).ModPath.FullName; - foreach (var (path, file) in option.FileData) + foreach (var (path, file) in option.Files) { var target = Path.GetRelativePath(parentPath, file.FullName); target = Path.Combine(newMod.FullName, target); @@ -348,7 +339,7 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - var (groupIdx, optionIdx) = option.GetIndices(); + var (groupIdx, optionIdx) = option.GetOptionIndices(); _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index a6218c6f..88e48f0f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager) Changes = true; } - public void Load(Mod mod, SubMod currentOption) + public void Load(Mod mod, IModDataContainer currentOption) { OtherImcCount = 0; OtherEqpCount = 0; @@ -111,7 +111,7 @@ public class ModMetaEditor(ModManager modManager) OtherGmpCount = 0; OtherEstCount = 0; OtherRspCount = 0; - foreach (var option in mod.AllSubMods) + foreach (var option in mod.AllDataContainers) { if (option == currentOption) continue; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 9698fdcb..db00a1c7 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -168,27 +169,11 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); - switch (group) - { - case SingleModGroup single: - _redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count); - for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i) - _redirections[groupIdx + 1].Add([]); - - foreach (var (option, optionIdx) in single.OptionData.WithIndex()) - HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); - - break; - case MultiModGroup multi: - _redirections[groupIdx + 1].EnsureCapacity(multi.PrioritizedOptions.Count); - for (var i = _redirections[groupIdx + 1].Count; i < multi.PrioritizedOptions.Count; ++i) - _redirections[groupIdx + 1].Add([]); - - foreach (var ((option, _), optionIdx) in multi.PrioritizedOptions.WithIndex()) - HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); - - break; - } + _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count); + for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i) + _redirections[groupIdx + 1].Add([]); + foreach (var (data, dataIdx) in group.DataContainers.WithIndex()) + HandleSubMod(groupDir, data, _redirections[groupIdx + 1][dataIdx]); } return true; @@ -200,13 +185,14 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) return false; - void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary newDict) + void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + var name = option.GetName(); + var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true); newDict.Clear(); - newDict.EnsureCapacity(option.FileData.Count); - foreach (var (gamePath, fullPath) in option.FileData) + newDict.EnsureCapacity(option.Files.Count); + foreach (var (gamePath, fullPath) in option.Files) { var relPath = new Utf8RelPath(gamePath).ToString(); var newFullPath = Path.Combine(optionDir.FullName, relPath); @@ -300,7 +286,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); break; case MultiModGroup multi: - foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) + foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); break; } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0d5f05a9..64788cf3 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager) public IReadOnlyDictionary Swaps => _swaps; - public void Revert(SubMod option) + public void Revert(IModDataContainer option) { _swaps.SetTo(option.FileSwaps); Changes = false; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index df243781..4f9e8648 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -176,13 +176,13 @@ public class ModCacheManager : IDisposable } private static void UpdateFileCount(Mod mod) - => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); + => mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count); private static void UpdateSwapCount(Mod mod) - => mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); + => mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count); private static void UpdateMetaCount(Mod mod) - => mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count); + => mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count); private static void UpdateHasOptions(Mod mod) => mod.HasOptions = mod.Groups.Any(o => o.IsOption); @@ -194,10 +194,10 @@ public class ModCacheManager : IDisposable { var changedItems = (SortedList)mod.ChangedItems; changedItems.Clear(); - foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) + foreach (var gamePath in mod.AllDataContainers.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) _identifier.Identify(changedItems, gamePath.ToString()); - foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) + foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier, changedItems, manip); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); @@ -211,20 +211,11 @@ public class ModCacheManager : IDisposable mod.HasOptions = false; foreach (var group in mod.Groups) { - mod.HasOptions |= group.IsOption; - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - - foreach (var s in optionEnumerator) - { - mod.TotalFileCount += s.Files.Count; - mod.TotalSwapCount += s.FileSwaps.Count; - mod.TotalManipulations += s.Manipulations.Count; - } + mod.HasOptions |= group.IsOption; + var (files, swaps, manips) = group.GetCounts(); + mod.TotalFileCount += files; + mod.TotalSwapCount += swaps; + mod.TotalManipulations += manips; } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 9c8ced89..8c4a5674 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -71,14 +71,14 @@ public static partial class ModMigration foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) { if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) - && !mod.Default.FileData.TryAdd(gamePath, unusedFile)) - Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}."); + && !mod.Default.Files.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.Files[gamePath]}."); } - mod.Default.FileSwapData.Clear(); - mod.Default.FileSwapData.EnsureCapacity(swaps.Count); + mod.Default.FileSwaps.Clear(); + mod.Default.FileSwaps.EnsureCapacity(swaps.Count); foreach (var (gamePath, swapPath) in swaps) - mod.Default.FileSwapData.Add(gamePath, swapPath); + mod.Default.FileSwaps.Add(gamePath, swapPath); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); foreach (var (_, index) in mod.Groups.WithIndex()) @@ -134,7 +134,7 @@ public static partial class ModMigration }; mod.Groups.Add(newMultiGroup); foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, newMultiGroup, option, seenMetaFiles), optionPriority++)); + newMultiGroup.OptionData.Add(SubModFromOption(creator, mod, newMultiGroup, option, optionPriority++, seenMetaFiles)); break; case GroupType.Single: @@ -158,22 +158,41 @@ public static partial class ModMigration } } - private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) + private static void AddFilesToSubMod(IModDataContainer mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) { foreach (var (relPath, gamePaths) in option.OptionFiles) { var fullPath = new FullPath(basePath, relPath); foreach (var gamePath in gamePaths) - mod.FileData.TryAdd(gamePath, fullPath); + mod.Files.TryAdd(gamePath, fullPath); if (fullPath.Extension is ".meta" or ".rgsp") seenMetaFiles.Add(fullPath); } } - private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet seenMetaFiles) + private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, + HashSet seenMetaFiles) { - var subMod = new SubMod(mod, group) { Name = option.OptionName }; + var subMod = new SingleSubMod(mod, group) + { + Name = option.OptionName, + Description = option.OptionDesc, + }; + AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + return subMod; + } + + private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, + ModPriority priority, HashSet seenMetaFiles) + { + var subMod = new MultiSubMod(mod, group) + { + Name = option.OptionName, + Description = option.OptionDesc, + Priority = priority, + }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index e78b6209..4d3a5717 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,4 +1,3 @@ -using System.Security.AccessControl; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -179,10 +178,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS switch (mod.Groups[groupIdx]) { case MultiModGroup multi: - if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) + if (multi.OptionData[optionIdx].Priority == newPriority) return; - multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); + multi.OptionData[optionIdx].Priority = newPriority; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; @@ -213,70 +212,62 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; - switch (group) - { - case SingleModGroup single: - { - var idx = single.OptionData.IndexOf(o => o.Name == newName); - if (idx >= 0) - return (single.OptionData[idx], idx, false); + var idx = group.Options.IndexOf(o => o.Name == newName); + if (idx >= 0) + return (group.Options[idx], idx, false); - idx = single.AddOption(mod, newName); - if (idx < 0) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + idx = group.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return (single.OptionData[^1], single.OptionData.Count - 1, true); - } - case MultiModGroup multi: - { - var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName); - if (idx >= 0) - return (multi.PrioritizedOptions[idx].Mod, idx, false); - - idx = multi.AddOption(mod, newName); - if (idx < 0) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true); - } - } - - throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}."); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (group.Options[idx], idx, true); } - /// Add an existing option to a given group with default priority. - public void AddOption(Mod mod, int groupIdx, SubMod option) - => AddOption(mod, groupIdx, option, ModPriority.Default); - - /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) + /// Add an existing option to a given group. + public void AddOption(Mod mod, int groupIdx, IModOption option) { var group = mod.Groups[groupIdx]; int idx; switch (group) { - case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: + case MultiModGroup { OptionData.Count: >= IModGroup.MaxMultiOptions }: Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: + { idx = s.OptionData.Count; - s.OptionData.Add(option); + var newOption = new SingleSubMod(s.Mod, s) + { + Name = option.Name, + Description = option.Description, + }; + if (option is IModDataContainer data) + IModDataContainer.Clone(data, newOption); + s.OptionData.Add(newOption); break; + } case MultiModGroup m: - idx = m.PrioritizedOptions.Count; - m.PrioritizedOptions.Add((option, priority)); + { + idx = m.OptionData.Count; + var newOption = new MultiSubMod(m.Mod, m) + { + Name = option.Name, + Description = option.Description, + Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, + }; + if (option is IModDataContainer data) + IModDataContainer.Clone(data, newOption); + m.OptionData.Add(newOption); break; - default: - return; + } + default: return; } saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); @@ -295,7 +286,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); + m.OptionData.RemoveAt(optionIdx); break; } @@ -315,59 +306,59 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations, + public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet manipulations, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); if (subMod.Manipulations.Count == manipulations.Count && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData.SetTo(manipulations); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.Manipulations.SetTo(manipulations); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1); } /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements, + public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary replacements, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + if (subMod.Files.SetEquals(replacements)) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData.SetTo(replacements); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.Files.SetTo(replacements); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, dataContainerIdx, -1); } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary additions) + public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary additions) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + var oldCount = subMod.Files.Count; + subMod.Files.AddFrom(additions); + if (oldCount != subMod.Files.Count) { saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1); } } /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps, + public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary swaps, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + if (subMod.FileSwaps.SetEquals(swaps)) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData.SetTo(swaps); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.FileSwaps.SetTo(swaps); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1); } @@ -389,17 +380,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Get the correct option for the given group and option index. - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx) { - if (groupIdx == -1 && optionIdx == 0) + if (groupIdx == -1 && dataContainerIdx == 0) return mod.Default; - return mod.Groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; + return mod.Groups[groupIdx].DataContainers[dataContainerIdx]; } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 71f64205..5c02213e 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -38,7 +38,7 @@ public sealed class Mod : IMod internal Mod(DirectoryInfo modPath) { ModPath = modPath; - Default = SubMod.CreateDefault(this); + Default = new DefaultSubMod(this); } public override string ToString() @@ -61,8 +61,8 @@ public sealed class Mod : IMod // Options - public readonly SubMod Default; - public readonly List Groups = []; + public readonly DefaultSubMod Default; + public readonly List Groups = []; public AppliedModData GetData(ModSettings? settings = null) { @@ -77,21 +77,16 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - Default.AddData(dictRedirections, setManips); + Default.AddDataTo(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } - public IEnumerable AllSubMods - => Groups.SelectMany(o => o switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod), - _ => [], - }).Prepend(Default); + public IEnumerable AllDataContainers + => Groups.SelectMany(o => o.DataContainers).Prepend(Default); public List FindUnusedFiles() { - var modFiles = AllSubMods.SelectMany(o => o.Files) + var modFiles = AllDataContainers.SelectMany(o => o.Files) .Select(p => p.Value) .ToHashSet(); return ModPath.EnumerateDirectories() diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 4d32f395..c1236037 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -112,10 +112,8 @@ public partial class ModCreator( var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); try { - if (!File.Exists(defaultFile)) - mod.Default.Load(mod.ModPath, new JObject(), out _); - else - mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); + var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); + IModDataContainer.Load(jObject, mod.Default, mod.ModPath); } catch (Exception e) { @@ -154,7 +152,7 @@ public partial class ModCreator( { var changes = false; List deleteList = new(); - foreach (var subMod in mod.AllSubMods) + foreach (var subMod in mod.AllDataContainers) { var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); changes |= localChanges; @@ -162,7 +160,7 @@ public partial class ModCreator( deleteList.AddRange(localDeleteList); } - SubMod.DeleteDeleteList(deleteList, delete); + IModDataContainer.DeleteDeleteList(deleteList, delete); if (!changes) return; @@ -176,10 +174,10 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) { var deleteList = new List(); - var oldSize = option.ManipulationData.Count; + var oldSize = option.Manipulations.Count; var deleteString = delete ? "with deletion." : "without deletion."; foreach (var (key, file) in option.Files.ToList()) { @@ -189,7 +187,7 @@ public partial class ModCreator( { if (ext1 == ".meta" || ext2 == ".meta") { - option.FileData.Remove(key); + option.Files.Remove(key); if (!file.Exists) continue; @@ -198,11 +196,11 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.ManipulationData.UnionWith(meta.MetaManipulations); + option.Manipulations.UnionWith(meta.MetaManipulations); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { - option.FileData.Remove(key); + option.Files.Remove(key); if (!file.Exists) continue; @@ -212,7 +210,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.ManipulationData.UnionWith(rgsp.MetaManipulations); + option.Manipulations.UnionWith(rgsp.MetaManipulations); } } catch (Exception e) @@ -221,8 +219,8 @@ public partial class ModCreator( } } - SubMod.DeleteDeleteList(deleteList, delete); - return (oldSize < option.ManipulationData.Count, deleteList); + IModDataContainer.DeleteDeleteList(deleteList, delete); + return (oldSize < option.Manipulations.Count, deleteList); } /// @@ -238,7 +236,7 @@ public partial class ModCreator( /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +246,7 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); + group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -258,7 +256,7 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods); + group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -266,16 +264,15 @@ public partial class ModCreator( } /// Create the data for a given sub mod from its data and the folder it is based on. - public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority) { var list = optionFolder.EnumerateNonHiddenFiles() .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = SubMod.CreateForSaving(option.Name); - mod.Description = option.Description; + var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority); foreach (var (_, gamePath, file) in list) - mod.FileData.TryAdd(gamePath, file); + mod.Files.TryAdd(gamePath, file); IncorporateMetaChanges(mod, baseFolder, true); return mod; @@ -292,7 +289,7 @@ public partial class ModCreator( foreach (var file in mod.FindUnusedFiles()) { if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) - mod.Default.FileData.TryAdd(gamePath, file); + mod.Default.Files.TryAdd(gamePath, file); } IncorporateMetaChanges(mod.Default, directory, true); diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/Subclasses/IModDataContainer.cs index d0b444b8..a26beb2a 100644 --- a/Penumbra/Mods/Subclasses/IModDataContainer.cs +++ b/Penumbra/Mods/Subclasses/IModDataContainer.cs @@ -1,12 +1,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; public interface IModDataContainer { + public IMod Mod { get; } + public IModGroup? Group { get; } + public Dictionary Files { get; set; } public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } @@ -21,6 +25,32 @@ public interface IModDataContainer manipulations.UnionWith(Manipulations); } + public string GetName() + => this switch + { + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, + _ => $"Container {GetDataIndices().DataIndex + 1}", + }; + + public string GetFullName() + => this switch + { + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, + _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", + }; + + public static void Clone(IModDataContainer from, IModDataContainer to) + { + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); + to.Manipulations = [.. from.Manipulations]; + } + + public (int GroupIndex, int DataIndex) GetDataIndices(); + public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) { data.Files.Clear(); @@ -77,4 +107,22 @@ public interface IModDataContainer serializer.Serialize(j, data.Manipulations); j.WriteEndObject(); } + + internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) + { + if (!delete) + return; + + foreach (var file in deleteList) + { + try + { + File.Delete(file); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); + } + } + } } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index a046ade0..5c500793 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; @@ -6,6 +7,11 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; +public interface ITexToolsGroup +{ + public IReadOnlyList OptionData { get; } +} + public interface IModGroup { public const int MaxMultiOptions = 63; @@ -19,28 +25,89 @@ public interface IModGroup public FullPath? FindBestMatch(Utf8GamePath gamePath); public int AddOption(Mod mod, string name, string description = ""); - public bool ChangeOptionDescription(int optionIndex, string newDescription); - public bool ChangeOptionName(int optionIndex, string newName); - public IReadOnlyList Options { get; } - public bool IsOption { get; } + public IReadOnlyList Options { get; } + public IReadOnlyList DataContainers { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public int GetIndex(); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null); + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= Options.Count) + return false; + + var option = Options[optionIndex]; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= Options.Count) + return false; + + var option = Options[optionIndex]; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) + { + jWriter.WriteStartObject(); + jWriter.WritePropertyName(nameof(group.Name)); + jWriter.WriteValue(group!.Name); + jWriter.WritePropertyName(nameof(group.Description)); + jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Priority)); + jWriter.WriteValue(group.Priority.Value); + jWriter.WritePropertyName(nameof(group.Type)); + jWriter.WriteValue(group.Type.ToString()); + jWriter.WritePropertyName(nameof(group.DefaultSettings)); + jWriter.WriteValue(group.DefaultSettings.Value); + } + + public (int Redirections, int Swaps, int Manips) GetCounts(); + + public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) + { + var redirectionCount = 0; + var swapCount = 0; + var manipCount = 0; + foreach (var option in group.DataContainers) + { + redirectionCount += option.Files.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; + } + + return (redirectionCount, swapCount, manipCount); + } } public readonly struct ModSaveGroup : ISavable { - private readonly DirectoryInfo _basePath; - private readonly IModGroup? _group; - private readonly int _groupIdx; - private readonly SubMod? _defaultMod; - private readonly bool _onlyAscii; + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly DefaultSubMod? _defaultMod; + private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) { @@ -61,7 +128,7 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; @@ -77,42 +144,11 @@ public readonly struct ModSaveGroup : ISavable using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); if (_groupIdx >= 0) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(_group.Name)); - j.WriteValue(_group!.Name); - j.WritePropertyName(nameof(_group.Description)); - j.WriteValue(_group.Description); - j.WritePropertyName(nameof(_group.Priority)); - j.WriteValue(_group.Priority.Value); - j.WritePropertyName(nameof(Type)); - j.WriteValue(_group.Type.ToString()); - j.WritePropertyName(nameof(_group.DefaultSettings)); - j.WriteValue(_group.DefaultSettings.Value); - switch (_group) - { - case SingleModGroup single: - j.WritePropertyName("Options"); - j.WriteStartArray(); - foreach (var option in single.OptionData) - SubMod.WriteSubMod(j, serializer, option, _basePath, null); - j.WriteEndArray(); - j.WriteEndObject(); - break; - case MultiModGroup multi: - j.WritePropertyName("Options"); - j.WriteStartArray(); - foreach (var (option, priority) in multi.PrioritizedOptions) - SubMod.WriteSubMod(j, serializer, option, _basePath, priority); - j.WriteEndArray(); - j.WriteEndObject(); - break; - } - } + _group!.WriteJson(j, serializer); else - { - SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); - } + IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + j.WriteEndObject(); } } diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/Subclasses/IModOption.cs index bb52a2cd..f66ce44e 100644 --- a/Penumbra/Mods/Subclasses/IModOption.cs +++ b/Penumbra/Mods/Subclasses/IModOption.cs @@ -15,6 +15,8 @@ public interface IModOption option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; } + public (int GroupIndex, int OptionIndex) GetOptionIndices(); + public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(Name)); diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 4ec2c72a..f194350a 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -10,20 +11,30 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow all available options to be selected at once. -public sealed class MultiModGroup(Mod mod) : IModGroup +public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { public GroupType Type => GroupType.Multi; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; set; } = mod; + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } + public readonly List OptionData = []; + + public IReadOnlyList Options + => OptionData; + + public IReadOnlyList DataContainers + => OptionData; + + public bool IsOption + => OptionData.Count > 0; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => PrioritizedOptions.OrderByDescending(o => o.Priority) - .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) + => OptionData.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); public int AddOption(Mod mod, string name, string description = "") @@ -32,49 +43,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup if (groupIdx < 0) return -1; - var subMod = new SubMod(mod, this) + var subMod = new MultiSubMod(mod, this) { Name = name, Description = description, }; - PrioritizedOptions.Add((subMod, ModPriority.Default)); - return PrioritizedOptions.Count - 1; + OptionData.Add(subMod); + return OptionData.Count - 1; } - public bool ChangeOptionDescription(int optionIndex, string newDescription) - { - if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) - return false; - - var option = PrioritizedOptions[optionIndex].Mod; - if (option.Description == newDescription) - return false; - - option.Description = newDescription; - return true; - } - - public bool ChangeOptionName(int optionIndex, string newName) - { - if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) - return false; - - var option = PrioritizedOptions[optionIndex].Mod; - if (option.Name == newName) - return false; - - option.Name = newName; - return true; - } - - public IReadOnlyList Options - => PrioritizedOptions.Select(p => p.Mod).ToArray(); - - public bool IsOption - => PrioritizedOptions.Count > 0; - - public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { var ret = new MultiModGroup(mod) @@ -91,7 +68,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup if (options != null) foreach (var child in options.Children()) { - if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) + if (ret.OptionData.Count == IModGroup.MaxMultiOptions) { Penumbra.Messager.NotificationMessage( $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", @@ -99,9 +76,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup break; } - var subMod = new SubMod(mod, ret); - subMod.Load(mod.ModPath, child, out var priority); - ret.PrioritizedOptions.Add((subMod, priority)); + var subMod = new MultiSubMod(mod, ret, child); + ret.OptionData.Add(subMod); } ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); @@ -115,39 +91,68 @@ public sealed class MultiModGroup(Mod mod) : IModGroup { case GroupType.Multi: return this; case GroupType.Single: - var multi = new SingleModGroup(Mod) + var single = new SingleModGroup(Mod) { Name = Name, Description = Description, Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), + DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; - multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); - return multi; + single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); + return single; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public bool MoveOption(int optionIdxFrom, int optionIdxTo) { - if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) + if (!OptionData.Move(optionIdxFrom, optionIdxTo)) return false; DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); return true; } + public int GetIndex() + { + var groupIndex = Mod.Groups.IndexOf(this); + if (groupIndex < 0) + throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); + + return groupIndex; + } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { - foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) + foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) { if (setting.HasFlag(index)) - option.Mod.AddData(redirections, manipulations); + option.AddDataTo(redirections, manipulations); } } + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + IModGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + IModOption.WriteModOption(jWriter, option); + jWriter.WritePropertyName(nameof(option.Priority)); + jWriter.WriteValue(option.Priority.Value); + IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => IModGroup.GetCountsBase(this); + public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); + => new(setting.Value & ((1ul << OptionData.Count) - 1)); /// Create a group without a mod only for saving it in the creator. internal static MultiModGroup CreateForSaving(string name) @@ -155,4 +160,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup { Name = name, }; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 994a1f96..d1a3b6d1 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; @@ -8,7 +9,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. -public sealed class SingleModGroup(Mod mod) : IModGroup +public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { public GroupType Type => GroupType.Single; @@ -19,16 +20,19 @@ public sealed class SingleModGroup(Mod mod) : IModGroup public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public readonly List OptionData = []; + public readonly List OptionData = []; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; public FullPath? FindBestMatch(Utf8GamePath gamePath) => OptionData - .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) + .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); public int AddOption(Mod mod, string name, string description = "") { - var subMod = new SubMod(mod, this) + var subMod = new SingleSubMod(mod, this) { Name = name, Description = description, @@ -37,35 +41,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup return OptionData.Count - 1; } - public bool ChangeOptionDescription(int optionIndex, string newDescription) - { - if (optionIndex < 0 || optionIndex >= OptionData.Count) - return false; - - var option = OptionData[optionIndex]; - if (option.Description == newDescription) - return false; - - option.Description = newDescription; - return true; - } - - public bool ChangeOptionName(int optionIndex, string newName) - { - if (optionIndex < 0 || optionIndex >= OptionData.Count) - return false; - - var option = OptionData[optionIndex]; - if (option.Name == newName) - return false; - - option.Name = newName; - return true; - } - public IReadOnlyList Options => OptionData; + public IReadOnlyList DataContainers + => OptionData; + public bool IsOption => OptionData.Count > 1; @@ -85,8 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SubMod(mod, ret); - subMod.Load(mod.ModPath, child, out _); + var subMod = new SingleSubMod(mod, ret, child); ret.OptionData.Add(subMod); } @@ -107,7 +87,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup Priority = Priority, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; - multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, new ModPriority(i)))); + multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); return multi; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } @@ -137,12 +117,39 @@ public sealed class SingleModGroup(Mod mod) : IModGroup return true; } + public int GetIndex() + { + var groupIndex = Mod.Groups.IndexOf(this); + if (groupIndex < 0) + throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); + + return groupIndex; + } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => OptionData[setting.AsIndex].AddData(redirections, manipulations); + => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); + public (int Redirections, int Swaps, int Manips) GetCounts() + => IModGroup.GetCountsBase(this); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + IModGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + IModOption.WriteModOption(jWriter, option); + IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + /// Create a group without a mod only for saving it in the creator. internal static SingleModGroup CreateForSaving(string name) => new(null!) diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index bc93fcc4..a2425eb7 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -7,7 +7,9 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer +public interface IModDataOption : IModOption, IModDataContainer; + +public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { internal readonly Mod Mod = mod; internal readonly SingleModGroup Group = group; @@ -19,12 +21,68 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataC public string Description { get; set; } = string.Empty; + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + public SingleSubMod(Mod mod, SingleModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + } + + public SingleSubMod Clone(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } } -public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer +public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { internal readonly Mod Mod = mod; internal readonly MultiModGroup Group = group; @@ -40,12 +98,76 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataCon public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + + public MultiSubMod(Mod mod, MultiModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + } + + public MultiSubMod Clone(Mod mod, MultiModGroup group) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = Priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) + => new(null!, null!) + { + Name = name, + Description = description, + Priority = priority, + }; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } } public class DefaultSubMod(IMod mod) : IModDataContainer { - public string FullName - => "Default Option"; + public const string FullName = "Default Option"; public string Description => string.Empty; @@ -55,183 +177,176 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup? IModDataContainer.Group + => null; + + + public DefaultSubMod(Mod mod, JToken json) + : this(mod) + { + IModDataContainer.Load(json, this, mod.ModPath); + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (-1, 0); } -/// -/// A sub mod is a collection of -/// - file replacements -/// - file swaps -/// - meta manipulations -/// that can be used either as an option or as the default data for a mod. -/// It can be loaded and reloaded from Json. -/// Nothing is checked for existence or validity when loading. -/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. -/// -public sealed class SubMod(IMod mod, IModGroup group) : IModOption -{ - public string Name { get; set; } = "Default"; - public string FullName - => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - internal readonly IMod Mod = mod; - internal readonly IModGroup? Group = group; - - internal (int GroupIdx, int OptionIdx) GetIndices() - { - if (IsDefault) - return (-1, 0); - - var groupIdx = Mod.Groups.IndexOf(Group); - if (groupIdx < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); - - return (groupIdx, GetOptionIndex()); - } - - private int GetOptionIndex() - { - var optionIndex = Group switch - { - null => 0, - SingleModGroup single => single.OptionData.IndexOf(this), - MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this), - _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), - }; - if (optionIndex < 0) - throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); - - return optionIndex; - } - - public static SubMod CreateDefault(IMod mod) - => new(mod, null!); - - [MemberNotNullWhen(false, nameof(Group))] - public bool IsDefault - => Group == null; - - public void AddData(Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); - } - - public Dictionary FileData = []; - public Dictionary FileSwapData = []; - public HashSet ManipulationData = []; - - public IReadOnlyDictionary Files - => FileData; - - public IReadOnlyDictionary FileSwaps - => FileSwapData; - - public IReadOnlySet Manipulations - => ManipulationData; - - public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) - { - FileData.Clear(); - FileSwapData.Clear(); - ManipulationData.Clear(); - - // Every option has a name, but priorities are only relevant for multi group options. - Name = json[nameof(Name)]?.ToObject() ?? string.Empty; - Description = json[nameof(Description)]?.ToObject() ?? string.Empty; - priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; - - var files = (JObject?)json[nameof(Files)]; - if (files != null) - foreach (var property in files.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); - } - - var swaps = (JObject?)json[nameof(FileSwaps)]; - if (swaps != null) - foreach (var property in swaps.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); - } - - var manips = json[nameof(Manipulations)]; - if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - ManipulationData.Add(s); - } - - internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) - { - if (!delete) - return; - - foreach (var file in deleteList) - { - try - { - File.Delete(file); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); - } - } - } - - /// Create a sub mod without a mod or group only for saving it in the creator. - internal static SubMod CreateForSaving(string name) - => new(null!, null!) - { - Name = name, - }; - - - public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(Name)); - j.WriteValue(mod.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(mod.Description); - if (priority != null) - { - j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value.Value); - } - - j.WritePropertyName(nameof(mod.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.Manipulations)); - serializer.Serialize(j, mod.Manipulations); - j.WriteEndObject(); - } -} +//public sealed class SubMod(IMod mod, IModGroup group) : IModOption +//{ +// public string Name { get; set; } = "Default"; +// +// public string FullName +// => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; +// +// public string Description { get; set; } = string.Empty; +// +// internal readonly IMod Mod = mod; +// internal readonly IModGroup? Group = group; +// +// internal (int GroupIdx, int OptionIdx) GetIndices() +// { +// if (IsDefault) +// return (-1, 0); +// +// var groupIdx = Mod.Groups.IndexOf(Group); +// if (groupIdx < 0) +// throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); +// +// return (groupIdx, GetOptionIndex()); +// } +// +// private int GetOptionIndex() +// { +// var optionIndex = Group switch +// { +// null => 0, +// SingleModGroup single => single.OptionData.IndexOf(this), +// MultiModGroup multi => multi.OptionData.IndexOf(p => p.Mod == this), +// _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), +// }; +// if (optionIndex < 0) +// throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); +// +// return optionIndex; +// } +// +// public static SubMod CreateDefault(IMod mod) +// => new(mod, null!); +// +// [MemberNotNullWhen(false, nameof(Group))] +// public bool IsDefault +// => Group == null; +// +// public void AddData(Dictionary redirections, HashSet manipulations) +// { +// foreach (var (path, file) in Files) +// redirections.TryAdd(path, file); +// +// foreach (var (path, file) in FileSwaps) +// redirections.TryAdd(path, file); +// manipulations.UnionWith(Manipulations); +// } +// +// public Dictionary FileData = []; +// public Dictionary FileSwapData = []; +// public HashSet ManipulationData = []; +// +// public IReadOnlyDictionary Files +// => FileData; +// +// public IReadOnlyDictionary FileSwaps +// => FileSwapData; +// +// public IReadOnlySet Manipulations +// => ManipulationData; +// +// public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) +// { +// FileData.Clear(); +// FileSwapData.Clear(); +// ManipulationData.Clear(); +// +// // Every option has a name, but priorities are only relevant for multi group options. +// Name = json[nameof(Name)]?.ToObject() ?? string.Empty; +// Description = json[nameof(Description)]?.ToObject() ?? string.Empty; +// priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; +// +// var files = (JObject?)json[nameof(Files)]; +// if (files != null) +// foreach (var property in files.Properties()) +// { +// if (Utf8GamePath.FromString(property.Name, out var p, true)) +// FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); +// } +// +// var swaps = (JObject?)json[nameof(FileSwaps)]; +// if (swaps != null) +// foreach (var property in swaps.Properties()) +// { +// if (Utf8GamePath.FromString(property.Name, out var p, true)) +// FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); +// } +// +// var manips = json[nameof(Manipulations)]; +// if (manips != null) +// foreach (var s in manips.Children().Select(c => c.ToObject()) +// .Where(m => m.Validate())) +// ManipulationData.Add(s); +// } +// +// +// /// Create a sub mod without a mod or group only for saving it in the creator. +// internal static SubMod CreateForSaving(string name) +// => new(null!, null!) +// { +// Name = name, +// }; +// +// +// public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) +// { +// j.WriteStartObject(); +// j.WritePropertyName(nameof(Name)); +// j.WriteValue(mod.Name); +// j.WritePropertyName(nameof(Description)); +// j.WriteValue(mod.Description); +// if (priority != null) +// { +// j.WritePropertyName(nameof(IModGroup.Priority)); +// j.WriteValue(priority.Value.Value); +// } +// +// j.WritePropertyName(nameof(mod.Files)); +// j.WriteStartObject(); +// foreach (var (gamePath, file) in mod.Files) +// { +// if (file.ToRelPath(basePath, out var relPath)) +// { +// j.WritePropertyName(gamePath.ToString()); +// j.WriteValue(relPath.ToString()); +// } +// } +// +// j.WriteEndObject(); +// j.WritePropertyName(nameof(mod.FileSwaps)); +// j.WriteStartObject(); +// foreach (var (gamePath, file) in mod.FileSwaps) +// { +// j.WritePropertyName(gamePath.ToString()); +// j.WriteValue(file.ToString()); +// } +// +// j.WriteEndObject(); +// j.WritePropertyName(nameof(mod.Manipulations)); +// serializer.Serialize(j, mod.Manipulations); +// j.WriteEndObject(); +// } +//} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index a599b3bb..393369d4 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -18,49 +18,46 @@ public class TemporaryMod : IMod public int TotalManipulations => Default.Manipulations.Count; - public readonly SubMod Default; + public readonly DefaultSubMod Default; public AppliedModData GetData(ModSettings? settings = null) { Dictionary dict; - if (Default.FileSwapData.Count == 0) + if (Default.FileSwaps.Count == 0) { - dict = Default.FileData; + dict = Default.Files; } - else if (Default.FileData.Count == 0) + else if (Default.Files.Count == 0) { - dict = Default.FileSwapData; + dict = Default.FileSwaps; } else { // Need to ensure uniqueness. - dict = new Dictionary(Default.FileData.Count + Default.FileSwaps.Count); - foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) + dict = new Dictionary(Default.Files.Count + Default.FileSwaps.Count); + foreach (var (gamePath, file) in Default.Files.Concat(Default.FileSwaps)) dict.TryAdd(gamePath, file); } - return new AppliedModData(dict, Default.ManipulationData); + return new AppliedModData(dict, Default.Manipulations); } public IReadOnlyList Groups => Array.Empty(); - public IEnumerable AllSubMods - => [Default]; - public TemporaryMod() - => Default = SubMod.CreateDefault(this); + => Default = new(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) - => Default.FileData[gamePath] = fullPath; + => Default.Files[gamePath] = fullPath; public bool SetManipulation(MetaManipulation manip) - => Default.ManipulationData.Remove(manip) | Default.ManipulationData.Add(manip); + => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); public void SetAll(Dictionary dict, HashSet manips) { - Default.FileData = dict; - Default.ManipulationData = manips; + Default.Files = dict; + Default.Manipulations = manips; } public static void SaveTempCollection(Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, @@ -93,16 +90,16 @@ public class TemporaryMod : IMod { var target = Path.Combine(fileDir.FullName, Path.GetFileName(targetPath)); File.Copy(targetPath, target, true); - defaultMod.FileData[gamePath] = new FullPath(target); + defaultMod.Files[gamePath] = new FullPath(target); } else { - defaultMod.FileSwapData[gamePath] = new FullPath(targetPath); + defaultMod.FileSwaps[gamePath] = new FullPath(targetPath); } } foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) - defaultMod.ManipulationData.Add(manip); + defaultMod.Manipulations.Add(manip); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c891d33a..503d64b7 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -305,7 +305,7 @@ public class FileEditor( UiHelpers.Text(gamePath.Path); ImGui.TableNextColumn(); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.TextUnformatted(option.FullName); + ImGui.TextUnformatted(option.GetFullName()); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index f765b47e..94f1d577 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -3,7 +3,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; @@ -79,7 +78,7 @@ public partial class ModEditWindow var file = f.RelPath.ToString(); return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) - : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, + : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(), _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); }); @@ -148,13 +147,13 @@ public partial class ModEditWindow (string, int) GetMulti() { var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); - return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length); + return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length); } var (text, groupCount) = color switch { ColorId.ConflictingMod => (string.Empty, 0), - ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1), + ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1), ColorId.InheritedMod => GetMulti(), _ => (string.Empty, 0), }; @@ -192,7 +191,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, IModDataContainer subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -228,7 +227,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); @@ -301,9 +300,9 @@ public partial class ModEditWindow tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { - var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); + var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); if (failedFiles > 0) - Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); + Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.GetFullName()}."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index aad70cb3..92a9dd66 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -45,7 +45,7 @@ public partial class ModEditWindow var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cca8fe10..6b9965b8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -85,7 +85,7 @@ public partial class ModEditWindow { // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? // NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. - return mod.AllSubMods + return mod.AllDataContainers .SelectMany(m => m.Files.Concat(m.FileSwaps)) .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) .Select(kv => kv.Key) @@ -103,7 +103,7 @@ public partial class ModEditWindow return []; // Filter then prepend the current option to ensure it's chosen first. - return mod.AllSubMods + return mod.AllDataContainers .Where(subMod => subMod != option) .Prepend(option) .SelectMany(subMod => subMod.Manipulations) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 4ecacece..70854fe7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -187,8 +187,8 @@ public partial class ModEditWindow if (editor == null) return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); - var subMod = editor.Option; - var optionName = subMod!.FullName; + var subMod = editor.Option!; + var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName; if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) return new QuickImportAction(editor, optionName, gamePath); @@ -199,7 +199,7 @@ public partial class ModEditWindow if (mod == null) return new QuickImportAction(editor, optionName, gamePath); - var (preferredPath, subDirs) = GetPreferredPath(mod, subMod, owner._config.ReplaceNonAsciiOnImport); + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport); var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; if (File.Exists(targetPath)) return new QuickImportAction(editor, optionName, gamePath); @@ -222,16 +222,16 @@ public partial class ModEditWindow { fileRegistry, }, _subDirs); - _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); + _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, IModOption? subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; - if (subMod == mod.Default) + if (subMod == null) return (path, subDirs); var name = subMod.Name; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 3f5f6c37..21d14f78 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -77,10 +77,10 @@ public partial class ModEditWindow : Window, IDisposable _forceTextureStartPath = true; } - public void ChangeOption(SubMod? subMod) + public void ChangeOption(IModDataContainer? subMod) { - var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); - _editor.LoadOption(groupIdx, optionIdx); + var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, dataIdx); } public void UpdateModels() @@ -111,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable }); var manipulations = 0; var subMods = 0; - var swaps = Mod!.AllSubMods.Sum(m => + var swaps = Mod!.AllDataContainers.Sum(m => { ++subMods; manipulations += m.Manipulations.Count; @@ -330,7 +330,7 @@ public partial class ModEditWindow : Window, IDisposable else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { _editor.ModNormalizer.Normalize(Mod!); - _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx), TaskScheduler.Default); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.DataIdx), TaskScheduler.Default); } if (!_editor.Duplicates.Worker.IsCompleted) @@ -405,7 +405,7 @@ public partial class ModEditWindow : Window, IDisposable var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); var ret = false; if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor.Option!.IsDefault)) + _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0); ret = true; @@ -414,7 +414,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) { - _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ret = true; } @@ -422,17 +422,17 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SetNextItemWidth(width.X); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); + using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName()); if (!combo) return ret; - foreach (var (option, idx) in Mod!.AllSubMods.WithIndex()) + foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex()) { using var id = ImRaii.PushId(idx); - if (ImGui.Selectable(option.FullName, option == _editor.Option)) + if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) { - var (groupIdx, optionIdx) = option.GetIndices(); - _editor.LoadOption(groupIdx, optionIdx); + var (groupIdx, dataIdx) = option.GetDataIndices(); + _editor.LoadOption(groupIdx, dataIdx); ret = true; } } @@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -569,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable } if (Mod != null) - foreach (var option in Mod.AllSubMods) + foreach (var option in Mod.AllDataContainers) { foreach (var path in option.Files.Keys) { diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index c34c7ef0..bed31ab8 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -50,8 +50,7 @@ public class ModMergeTab(ModMerger modMerger) ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); - var width = ImGui.GetItemRectSize(); - using (var g = ImRaii.Group()) + using (ImRaii.Group()) { using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; @@ -124,13 +123,13 @@ public class ModMergeTab(ModMerger modMerger) ImGui.Dummy(Vector2.One); var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); if (ImGui.Button("Select All", buttonSize)) - modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers); ImGui.SameLine(); if (ImGui.Button("Unselect All", buttonSize)) modMerger.SelectedOptions.Clear(); ImGui.SameLine(); if (ImGui.Button("Invert Selection", buttonSize)) - modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers); DrawOptionTable(size); } @@ -144,7 +143,7 @@ public class ModMergeTab(ModMerger modMerger) private void DrawOptionTable(float size) { - var options = modMerger.MergeFromMod!.AllSubMods.ToList(); + var options = modMerger.MergeFromMod!.AllDataContainers.ToList(); var height = modMerger.Warnings.Count == 0 && modMerger.Error == null ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() : 8 * ImGui.GetFrameHeightWithSpacing(); @@ -176,47 +175,41 @@ public class ModMergeTab(ModMerger modMerger) if (ImGui.Checkbox("##check", ref selected)) Handle(option, selected); - if (option.IsDefault) + if (option.Group is not { } group) { - ImGuiUtil.DrawTableColumn(option.FullName); + ImGuiUtil.DrawTableColumn(option.GetFullName()); ImGui.TableNextColumn(); } else { - ImGuiUtil.DrawTableColumn(option.Name); - var group = option.Group; - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; + ImGuiUtil.DrawTableColumn(option.GetName()); + ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) { if (ImGui.MenuItem("Select All")) // ReSharper disable once PossibleMultipleEnumeration - foreach (var opt in optionEnumerator) + foreach (var opt in group.DataContainers) Handle(opt, true); if (ImGui.MenuItem("Unselect All")) // ReSharper disable once PossibleMultipleEnumeration - foreach (var opt in optionEnumerator) + foreach (var opt in group.DataContainers) Handle(opt, false); ImGui.EndPopup(); } } ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); continue; - void Handle(SubMod option2, bool selected2) + void Handle(IModDataContainer option2, bool selected2) { if (selected2) modMerger.SelectedOptions.Add(option2); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 0dc694d8..c05f1ac1 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -486,7 +486,7 @@ public class ModPanelEditTab( EditOption(panel, single, groupIdx, optionIdx); break; case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) EditOption(panel, multi, groupIdx, optionIdx); break; } @@ -542,7 +542,7 @@ public class ModPanelEditTab( if (group is not MultiModGroup multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority, 50 * UiHelpers.Scale)) panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); @@ -557,7 +557,7 @@ public class ModPanelEditTab( var count = group switch { SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.PrioritizedOptions.Count, + MultiModGroup multi => multi.OptionData.Count, _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), }; ImGui.TableNextColumn(); @@ -591,6 +591,9 @@ public class ModPanelEditTab( // Handle drag and drop to move options inside a group or into another group. private static void Source(IModGroup group, int groupIdx, int optionIdx) { + if (group is not ITexToolsGroup) + return; + using var source = ImRaii.DragDropSource(); if (!source) return; @@ -606,6 +609,9 @@ public class ModPanelEditTab( private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) { + if (group is not ITexToolsGroup) + return; + using var target = ImRaii.DragDropTarget(); if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) return; @@ -624,22 +630,12 @@ public class ModPanelEditTab( var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group switch - { - SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.PrioritizedOptions.Count, - _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), - }; - var (option, priority) = sourceGroup switch - { - SingleModGroup single => (single.OptionData[_dragDropOptionIdx], ModPriority.Default), - MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx], - _ => throw new Exception($"Dragging options from an option group of type {sourceGroup.GetType()} is not supported."), - }; + var currentCount = group.DataContainers.Count; + var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx]; panel._delayedActions.Enqueue(() => { panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); }); } diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 02ec9a32..1aaa7741 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -114,7 +114,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption((SubMod)mod.Default); + _modEditWindow.ChangeOption(mod.Default); _modEditWindow.IsOpen = true; } From 514121d8c133baf711c19a9ca1dfa585f6043f6d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:28:12 +0200 Subject: [PATCH 060/865] Reorder stuff. --- Penumbra/Api/Api/ModSettingsApi.cs | 3 +- Penumbra/Api/Api/TemporaryApi.cs | 2 +- Penumbra/Api/TempModManager.cs | 2 +- .../Cache/CollectionCacheManager.cs | 2 +- .../Collections/Manager/CollectionEditor.cs | 2 +- .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/ModCollectionMigration.cs | 2 +- Penumbra/Collections/ModCollection.cs | 2 +- Penumbra/Collections/ModCollectionSave.cs | 2 +- Penumbra/Communication/ModSettingChanged.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 4 +- Penumbra/Meta/MetaFileManager.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 3 +- Penumbra/Mods/Editor/FileRegistry.cs | 2 +- Penumbra/Mods/Editor/IMod.cs | 3 +- Penumbra/Mods/Editor/ModEditor.cs | 3 +- Penumbra/Mods/Editor/ModFileCollection.cs | 2 +- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 3 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- .../Mods/{Subclasses => Groups}/IModGroup.cs | 86 +---- Penumbra/Mods/Groups/ModSaveGroup.cs | 57 +++ .../{Subclasses => Groups}/MultiModGroup.cs | 30 +- .../{Subclasses => Groups}/SingleModGroup.cs | 28 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 1 - Penumbra/Mods/Manager/ModMigration.cs | 4 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +- Penumbra/Mods/Mod.cs | 5 +- Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Mods/ModLocalData.cs | 20 +- Penumbra/Mods/ModMeta.cs | 24 +- .../{Subclasses => Settings}/ModPriority.cs | 2 +- .../{Subclasses => Settings}/ModSettings.cs | 5 +- .../Mods/{Subclasses => Settings}/Setting.cs | 2 +- .../{Subclasses => Settings}/SettingList.cs | 2 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 30 ++ .../IModDataContainer.cs | 27 +- .../{Subclasses => SubMods}/IModOption.cs | 8 +- Penumbra/Mods/SubMods/MultiSubMod.cs | 92 +++++ Penumbra/Mods/SubMods/SingleSubMod.cs | 82 ++++ Penumbra/Mods/Subclasses/SubMod.cs | 352 ------------------ Penumbra/Mods/TemporaryMod.cs | 6 +- Penumbra/Penumbra.csproj | 4 + Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/Services/SaveService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 2 +- .../ModEditWindow.QuickImport.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 3 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 1 - 58 files changed, 416 insertions(+), 539 deletions(-) rename Penumbra/Mods/{Subclasses => Groups}/IModGroup.cs (50%) create mode 100644 Penumbra/Mods/Groups/ModSaveGroup.cs rename Penumbra/Mods/{Subclasses => Groups}/MultiModGroup.cs (83%) rename Penumbra/Mods/{Subclasses => Groups}/SingleModGroup.cs (85%) rename Penumbra/Mods/{Subclasses => Settings}/ModPriority.cs (98%) rename Penumbra/Mods/{Subclasses => Settings}/ModSettings.cs (98%) rename Penumbra/Mods/{Subclasses => Settings}/Setting.cs (98%) rename Penumbra/Mods/{Subclasses => Settings}/SettingList.cs (97%) create mode 100644 Penumbra/Mods/SubMods/DefaultSubMod.cs rename Penumbra/Mods/{Subclasses => SubMods}/IModDataContainer.cs (80%) rename Penumbra/Mods/{Subclasses => SubMods}/IModOption.cs (72%) create mode 100644 Penumbra/Mods/SubMods/MultiSubMod.cs create mode 100644 Penumbra/Mods/SubMods/SingleSubMod.cs delete mode 100644 Penumbra/Mods/Subclasses/SubMod.cs diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index e145e027..039fbfa9 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -8,8 +8,9 @@ using Penumbra.Communication; using Penumbra.Interop.PathResolving; using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Api.Api; diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index b4ffa8f4..38d080cc 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -5,7 +5,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.String.Classes; namespace Penumbra.Api.Api; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 7d682338..aee2b447 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -6,7 +6,7 @@ using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Collections.Manager; using Penumbra.Communication; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Api; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 4b5c4337..c1296414 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -8,7 +8,7 @@ using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 4af19e6b..0243de1e 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -2,7 +2,7 @@ using OtterGui; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 2da2a569..4e2fb7b7 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -5,7 +5,7 @@ using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Collections.Manager; diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index 053f0a2b..89743aa2 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -1,6 +1,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.Util; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index e666b151..4580e37a 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,7 +1,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; namespace Penumbra.Collections; diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index acc38d83..e6bb069b 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Linq; using Penumbra.Services; using Newtonsoft.Json; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Collections; diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 968f78a7..a7da345b 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -4,7 +4,7 @@ using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Communication; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index b9cdda71..eb6d0b0c 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -3,7 +3,9 @@ using OtterGui; using Penumbra.Api.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Util; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 0e2e638b..cd99396b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -11,7 +11,7 @@ using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; using Penumbra.Services; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 92ec58b9..31aacbe1 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 44d349ce..a484c8c2 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -1,4 +1,4 @@ -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index c4c4be2f..d4c881e9 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 1118f890..e1c5962f 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,6 +1,7 @@ using OtterGui; using OtterGui.Compression; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index ede35914..551d04cf 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,5 +1,5 @@ using OtterGui; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 11e35334..00685c94 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,5 +1,5 @@ using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 541c84ae..0f629bc7 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -5,7 +5,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.ModsTab; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 88e48f0f..dee700d5 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,6 +1,6 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index db00a1c7..e2088b32 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -3,8 +3,9 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; using OtterGui.Tasks; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 64788cf3..3247cfdf 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,6 +1,6 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs similarity index 50% rename from Penumbra/Mods/Subclasses/IModGroup.cs rename to Penumbra/Mods/Groups/IModGroup.cs index 5c500793..dc5150cf 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -1,11 +1,11 @@ -using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; -using Penumbra.Services; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Groups; public interface ITexToolsGroup { @@ -16,22 +16,22 @@ public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; } + public string Description { get; } + public GroupType Type { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public int AddOption(Mod mod, string name, string description = ""); - public IReadOnlyList Options { get; } + public IReadOnlyList Options { get; } public IReadOnlyList DataContainers { get; } - public bool IsOption { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); - public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public bool MoveOption(int optionIdxFrom, int optionIdxTo); public int GetIndex(); @@ -88,67 +88,15 @@ public interface IModGroup public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) { var redirectionCount = 0; - var swapCount = 0; - var manipCount = 0; + var swapCount = 0; + var manipCount = 0; foreach (var option in group.DataContainers) { redirectionCount += option.Files.Count; - swapCount += option.FileSwaps.Count; - manipCount += option.Manipulations.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; } return (redirectionCount, swapCount, manipCount); } } - -public readonly struct ModSaveGroup : ISavable -{ - private readonly DirectoryInfo _basePath; - private readonly IModGroup? _group; - private readonly int _groupIdx; - private readonly DefaultSubMod? _defaultMod; - private readonly bool _onlyAscii; - - public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) - { - _basePath = mod.ModPath; - _groupIdx = groupIdx; - if (_groupIdx < 0) - _defaultMod = mod.Default; - else - _group = mod.Groups[_groupIdx]; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) - { - _basePath = basePath; - _group = group; - _groupIdx = groupIdx; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) - { - _basePath = basePath; - _groupIdx = -1; - _defaultMod = @default; - _onlyAscii = onlyAscii; - } - - public string ToFilename(FilenameService fileNames) - => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); - - public void Save(StreamWriter writer) - { - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; - j.WriteStartObject(); - if (_groupIdx >= 0) - _group!.WriteJson(j, serializer); - else - IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); - j.WriteEndObject(); - } -} diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs new file mode 100644 index 00000000..ed81f42f --- /dev/null +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Groups; + +public readonly struct ModSaveGroup : ISavable +{ + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly DefaultSubMod? _defaultMod; + private readonly bool _onlyAscii; + + public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) + { + _basePath = mod.ModPath; + _groupIdx = groupIdx; + if (_groupIdx < 0) + _defaultMod = mod.Default; + else + _group = mod.Groups[_groupIdx]; + _onlyAscii = onlyAscii; + } + + public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) + { + _basePath = basePath; + _group = group; + _groupIdx = groupIdx; + _onlyAscii = onlyAscii; + } + + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) + { + _basePath = basePath; + _groupIdx = -1; + _defaultMod = @default; + _onlyAscii = onlyAscii; + } + + public string ToFilename(FilenameService fileNames) + => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); + if (_groupIdx >= 0) + _group!.WriteJson(j, serializer); + else + IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs similarity index 83% rename from Penumbra/Mods/Subclasses/MultiModGroup.cs rename to Penumbra/Mods/Groups/MultiModGroup.cs index f194350a..f39f2e70 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -6,9 +6,11 @@ using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Groups; /// Groups that allow all available options to be selected at once. public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup @@ -16,11 +18,11 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; set; } = mod; + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; public IReadOnlyList Options @@ -45,7 +47,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup var subMod = new MultiSubMod(mod, this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); @@ -56,9 +58,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { var ret = new MultiModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -93,9 +95,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup case GroupType.Single: var single = new SingleModGroup(Mod) { - Name = Name, - Description = Description, - Priority = Priority, + Name = Name, + Description = Description, + Priority = Priority, DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); @@ -152,7 +154,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => IModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << OptionData.Count) - 1)); + => new(setting.Value & (1ul << OptionData.Count) - 1); /// Create a group without a mod only for saving it in the creator. internal static MultiModGroup CreateForSaving(string name) diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs similarity index 85% rename from Penumbra/Mods/Subclasses/SingleModGroup.cs rename to Penumbra/Mods/Groups/SingleModGroup.cs index d1a3b6d1..6011185a 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -4,9 +4,11 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Groups; /// Groups that allow only one of their available options to be selected. public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup @@ -14,11 +16,11 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; set; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -34,7 +36,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { var subMod = new SingleSubMod(mod, this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); @@ -55,9 +57,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup var options = json["Options"]; var ret = new SingleModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -82,9 +84,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup case GroupType.Multi: var multi = new MultiModGroup(Mod) { - Name = Name, - Description = Description, - Priority = Priority, + Name = Name, + Description = Description, + Priority = Priority, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 21b9ef2c..1545811e 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -7,7 +7,7 @@ using Penumbra.String.Classes; using Penumbra.Meta; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; namespace Penumbra.Mods.ItemSwap; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 4f9e8648..3ff1a333 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,7 +2,6 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 8c4a5674..f160d5bd 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -2,7 +2,9 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 4d3a5717..b66fec17 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -4,7 +4,9 @@ using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 5c02213e..fc84afcc 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,9 +1,10 @@ using OtterGui; using OtterGui.Classes; -using Penumbra.Collections.Cache; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index c1236037..e8113ee1 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -9,8 +9,10 @@ using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index 51fe8d58..beda0dc7 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -5,29 +5,25 @@ using Penumbra.Services; namespace Penumbra.Mods; -public readonly struct ModLocalData : ISavable +public readonly struct ModLocalData(Mod mod) : ISavable { public const int FileVersion = 3; - private readonly Mod _mod; - - public ModLocalData(Mod mod) - => _mod = mod; - public string ToFilename(FilenameService fileNames) - => fileNames.LocalDataFile(_mod); + => fileNames.LocalDataFile(mod); public void Save(StreamWriter writer) { var jObject = new JObject { { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) }, - { nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) }, - { nameof(Mod.Note), JToken.FromObject(_mod.Note) }, - { nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) }, + { nameof(Mod.ImportDate), JToken.FromObject(mod.ImportDate) }, + { nameof(Mod.LocalTags), JToken.FromObject(mod.LocalTags) }, + { nameof(Mod.Note), JToken.FromObject(mod.Note) }, + { nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) }, }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); } diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index d29cdb9c..870d6d4f 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -4,31 +4,27 @@ using Penumbra.Services; namespace Penumbra.Mods; -public readonly struct ModMeta : ISavable +public readonly struct ModMeta(Mod mod) : ISavable { public const uint FileVersion = 3; - private readonly Mod _mod; - - public ModMeta(Mod mod) - => _mod = mod; - public string ToFilename(FilenameService fileNames) - => fileNames.ModMetaPath(_mod); + => fileNames.ModMetaPath(mod); public void Save(StreamWriter writer) { var jObject = new JObject { { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(Mod.Name), JToken.FromObject(_mod.Name) }, - { nameof(Mod.Author), JToken.FromObject(_mod.Author) }, - { nameof(Mod.Description), JToken.FromObject(_mod.Description) }, - { nameof(Mod.Version), JToken.FromObject(_mod.Version) }, - { nameof(Mod.Website), JToken.FromObject(_mod.Website) }, - { nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) }, + { nameof(Mod.Name), JToken.FromObject(mod.Name) }, + { nameof(Mod.Author), JToken.FromObject(mod.Author) }, + { nameof(Mod.Description), JToken.FromObject(mod.Description) }, + { nameof(Mod.Version), JToken.FromObject(mod.Version) }, + { nameof(Mod.Website), JToken.FromObject(mod.Website) }, + { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); } } diff --git a/Penumbra/Mods/Subclasses/ModPriority.cs b/Penumbra/Mods/Settings/ModPriority.cs similarity index 98% rename from Penumbra/Mods/Subclasses/ModPriority.cs rename to Penumbra/Mods/Settings/ModPriority.cs index a99c12ed..993bd577 100644 --- a/Penumbra/Mods/Subclasses/ModPriority.cs +++ b/Penumbra/Mods/Settings/ModPriority.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; [JsonConverter(typeof(Converter))] public readonly record struct ModPriority(int Value) : diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs similarity index 98% rename from Penumbra/Mods/Subclasses/ModSettings.cs rename to Penumbra/Mods/Settings/ModSettings.cs index 2ddabdb8..db9e0521 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -1,12 +1,11 @@ using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; /// Contains the settings for a given mod. public class ModSettings diff --git a/Penumbra/Mods/Subclasses/Setting.cs b/Penumbra/Mods/Settings/Setting.cs similarity index 98% rename from Penumbra/Mods/Subclasses/Setting.cs rename to Penumbra/Mods/Settings/Setting.cs index 18b1e4ca..231529b8 100644 --- a/Penumbra/Mods/Subclasses/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using OtterGui; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; [JsonConverter(typeof(Converter))] public readonly record struct Setting(ulong Value) diff --git a/Penumbra/Mods/Subclasses/SettingList.cs b/Penumbra/Mods/Settings/SettingList.cs similarity index 97% rename from Penumbra/Mods/Subclasses/SettingList.cs rename to Penumbra/Mods/Settings/SettingList.cs index ea1e447f..67b1b947 100644 --- a/Penumbra/Mods/Subclasses/SettingList.cs +++ b/Penumbra/Mods/Settings/SettingList.cs @@ -1,4 +1,4 @@ -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.Settings; public class SettingList : List { diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs new file mode 100644 index 00000000..ced0cd0d --- /dev/null +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public class DefaultSubMod(IMod mod) : IModDataContainer +{ + public const string FullName = "Default Option"; + + internal readonly IMod Mod = mod; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup? IModDataContainer.Group + => null; + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (-1, 0); +} diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs similarity index 80% rename from Penumbra/Mods/Subclasses/IModDataContainer.cs rename to Penumbra/Mods/SubMods/IModDataContainer.cs index a26beb2a..18b3b23f 100644 --- a/Penumbra/Mods/Subclasses/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -2,18 +2,19 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.String.Classes; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.SubMods; public interface IModDataContainer { - public IMod Mod { get; } + public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } public void AddDataTo(Dictionary redirections, HashSet manipulations) { @@ -28,24 +29,24 @@ public interface IModDataContainer public string GetName() => this switch { - IModOption o => o.FullName, + IModOption o => o.FullName, DefaultSubMod => DefaultSubMod.FullName, - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; public string GetFullName() => this switch { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; public static void Clone(IModDataContainer from, IModDataContainer to) { - to.Files = new Dictionary(from.Files); - to.FileSwaps = new Dictionary(from.FileSwaps); + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); to.Manipulations = [.. from.Manipulations]; } @@ -126,3 +127,5 @@ public interface IModDataContainer } } } + +public interface IModDataOption : IModOption, IModDataContainer; diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs similarity index 72% rename from Penumbra/Mods/Subclasses/IModOption.cs rename to Penumbra/Mods/SubMods/IModOption.cs index f66ce44e..f1ce1d4c 100644 --- a/Penumbra/Mods/Subclasses/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,17 +1,17 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Penumbra.Mods.Subclasses; +namespace Penumbra.Mods.SubMods; public interface IModOption { - public string Name { get; set; } - public string FullName { get; } + public string Name { get; set; } + public string FullName { get; } public string Description { get; set; } public static void Load(JToken json, IModOption option) { - option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; } diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs new file mode 100644 index 00000000..00216b77 --- /dev/null +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption +{ + internal readonly Mod Mod = mod; + internal readonly MultiModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + public ModPriority Priority { get; set; } = ModPriority.Default; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + + public MultiSubMod(Mod mod, MultiModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + } + + public MultiSubMod Clone(Mod mod, MultiModGroup group) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = Priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) + => new(null!, null!) + { + Name = name, + Description = description, + Priority = priority, + }; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs new file mode 100644 index 00000000..499ab192 --- /dev/null +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption +{ + internal readonly Mod Mod = mod; + internal readonly SingleModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + public SingleSubMod(Mod mod, SingleModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + } + + public SingleSubMod Clone(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } +} diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs deleted file mode 100644 index a2425eb7..00000000 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ /dev/null @@ -1,352 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.Subclasses; - -public interface IModDataOption : IModOption, IModDataContainer; - -public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption -{ - internal readonly Mod Mod = mod; - internal readonly SingleModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - public SingleSubMod(Mod mod, SingleModGroup group, JToken json) - : this(mod, group) - { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); - } - - public SingleSubMod Clone(Mod mod, SingleModGroup group) - { - var ret = new SingleSubMod(mod, group) - { - Name = Name, - Description = Description, - }; - IModDataContainer.Clone(this, ret); - - return ret; - } - - public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) - { - var ret = new MultiSubMod(mod, group) - { - Name = Name, - Description = Description, - Priority = priority, - }; - IModDataContainer.Clone(this, ret); - - return ret; - } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} - -public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption -{ - internal readonly Mod Mod = mod; - internal readonly MultiModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - public ModPriority Priority { get; set; } = ModPriority.Default; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) - : this(mod, group) - { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); - Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; - } - - public MultiSubMod Clone(Mod mod, MultiModGroup group) - { - var ret = new MultiSubMod(mod, group) - { - Name = Name, - Description = Description, - Priority = Priority, - }; - IModDataContainer.Clone(this, ret); - - return ret; - } - - public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) - { - var ret = new SingleSubMod(mod, group) - { - Name = Name, - Description = Description, - }; - IModDataContainer.Clone(this, ret); - return ret; - } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); - - public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) - => new(null!, null!) - { - Name = name, - Description = description, - Priority = priority, - }; - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} - -public class DefaultSubMod(IMod mod) : IModDataContainer -{ - public const string FullName = "Default Option"; - - public string Description - => string.Empty; - - internal readonly IMod Mod = mod; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup? IModDataContainer.Group - => null; - - - public DefaultSubMod(Mod mod, JToken json) - : this(mod) - { - IModDataContainer.Load(json, this, mod.ModPath); - } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (-1, 0); -} - - -//public sealed class SubMod(IMod mod, IModGroup group) : IModOption -//{ -// public string Name { get; set; } = "Default"; -// -// public string FullName -// => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; -// -// public string Description { get; set; } = string.Empty; -// -// internal readonly IMod Mod = mod; -// internal readonly IModGroup? Group = group; -// -// internal (int GroupIdx, int OptionIdx) GetIndices() -// { -// if (IsDefault) -// return (-1, 0); -// -// var groupIdx = Mod.Groups.IndexOf(Group); -// if (groupIdx < 0) -// throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); -// -// return (groupIdx, GetOptionIndex()); -// } -// -// private int GetOptionIndex() -// { -// var optionIndex = Group switch -// { -// null => 0, -// SingleModGroup single => single.OptionData.IndexOf(this), -// MultiModGroup multi => multi.OptionData.IndexOf(p => p.Mod == this), -// _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), -// }; -// if (optionIndex < 0) -// throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); -// -// return optionIndex; -// } -// -// public static SubMod CreateDefault(IMod mod) -// => new(mod, null!); -// -// [MemberNotNullWhen(false, nameof(Group))] -// public bool IsDefault -// => Group == null; -// -// public void AddData(Dictionary redirections, HashSet manipulations) -// { -// foreach (var (path, file) in Files) -// redirections.TryAdd(path, file); -// -// foreach (var (path, file) in FileSwaps) -// redirections.TryAdd(path, file); -// manipulations.UnionWith(Manipulations); -// } -// -// public Dictionary FileData = []; -// public Dictionary FileSwapData = []; -// public HashSet ManipulationData = []; -// -// public IReadOnlyDictionary Files -// => FileData; -// -// public IReadOnlyDictionary FileSwaps -// => FileSwapData; -// -// public IReadOnlySet Manipulations -// => ManipulationData; -// -// public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) -// { -// FileData.Clear(); -// FileSwapData.Clear(); -// ManipulationData.Clear(); -// -// // Every option has a name, but priorities are only relevant for multi group options. -// Name = json[nameof(Name)]?.ToObject() ?? string.Empty; -// Description = json[nameof(Description)]?.ToObject() ?? string.Empty; -// priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; -// -// var files = (JObject?)json[nameof(Files)]; -// if (files != null) -// foreach (var property in files.Properties()) -// { -// if (Utf8GamePath.FromString(property.Name, out var p, true)) -// FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); -// } -// -// var swaps = (JObject?)json[nameof(FileSwaps)]; -// if (swaps != null) -// foreach (var property in swaps.Properties()) -// { -// if (Utf8GamePath.FromString(property.Name, out var p, true)) -// FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject()!)); -// } -// -// var manips = json[nameof(Manipulations)]; -// if (manips != null) -// foreach (var s in manips.Children().Select(c => c.ToObject()) -// .Where(m => m.Validate())) -// ManipulationData.Add(s); -// } -// -// -// /// Create a sub mod without a mod or group only for saving it in the creator. -// internal static SubMod CreateForSaving(string name) -// => new(null!, null!) -// { -// Name = name, -// }; -// -// -// public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) -// { -// j.WriteStartObject(); -// j.WritePropertyName(nameof(Name)); -// j.WriteValue(mod.Name); -// j.WritePropertyName(nameof(Description)); -// j.WriteValue(mod.Description); -// if (priority != null) -// { -// j.WritePropertyName(nameof(IModGroup.Priority)); -// j.WriteValue(priority.Value.Value); -// } -// -// j.WritePropertyName(nameof(mod.Files)); -// j.WriteStartObject(); -// foreach (var (gamePath, file) in mod.Files) -// { -// if (file.ToRelPath(basePath, out var relPath)) -// { -// j.WritePropertyName(gamePath.ToString()); -// j.WriteValue(relPath.ToString()); -// } -// } -// -// j.WriteEndObject(); -// j.WritePropertyName(nameof(mod.FileSwaps)); -// j.WriteStartObject(); -// foreach (var (gamePath, file) in mod.FileSwaps) -// { -// j.WritePropertyName(gamePath.ToString()); -// j.WriteValue(file.ToString()); -// } -// -// j.WriteEndObject(); -// j.WritePropertyName(nameof(mod.Manipulations)); -// serializer.Serialize(j, mod.Manipulations); -// j.WriteEndObject(); -// } -//} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 393369d4..d08c8b06 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -2,8 +2,10 @@ using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; @@ -46,7 +48,7 @@ public class TemporaryMod : IMod => Array.Empty(); public TemporaryMod() - => Default = new(this); + => Default = new DefaultSubMod(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) => Default.Files[gamePath] = fullPath; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index c8961579..0ec1fd44 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -93,6 +93,10 @@ + + + + diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index e775d81a..fafaa0e5 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -10,7 +10,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.UI.ResourceWatcher; diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 801e0c1d..8d3cb641 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -2,7 +2,7 @@ using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; using Penumbra.Mods; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Groups; namespace Penumbra.Services; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 5125a5b2..77bdb161 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -13,9 +13,10 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 94f1d577..107c56e6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -4,7 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 70854fe7..55b7e748 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -8,7 +8,7 @@ using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Mods; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 21d14f78..dbb88fb7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -21,7 +21,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index bed31ab8..5dad66b4 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -4,7 +4,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.SubMods; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 11a2d96f..5b6cfa99 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -14,7 +14,7 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; using MessageService = Penumbra.Services.MessageService; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index b2cfaf25..5e3aac48 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -9,7 +9,7 @@ using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Editor; -using Penumbra.Mods.Subclasses; +using Penumbra.Mods.Settings; using Penumbra.String.Classes; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index c05f1ac1..afbef45d 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -11,9 +11,10 @@ using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index cb76088c..1326a763 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -8,8 +8,10 @@ using Penumbra.UI.Classes; using Dalamud.Interface.Components; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.Services; +using Penumbra.Mods.SubMods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 1aaa7741..8b09d8b9 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -5,7 +5,6 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Mods.Subclasses; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; From cd76c31d8ce73402b40e45d73dace053cc8a28b5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:41:55 +0200 Subject: [PATCH 061/865] Fix stack overflow. --- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/IModDataContainer.cs | 8 ++++---- Penumbra/Mods/SubMods/MultiSubMod.cs | 2 +- Penumbra/Mods/SubMods/SingleSubMod.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index ced0cd0d..8b166505 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -23,7 +23,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer => null; public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + => IModDataContainer.AddDataTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (-1, 0); diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 18b3b23f..c9420821 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -16,14 +16,14 @@ public interface IModDataContainer public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } - public void AddDataTo(Dictionary redirections, HashSet manipulations) + public static void AddDataTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) { - foreach (var (path, file) in Files) + foreach (var (path, file) in container.Files) redirections.TryAdd(path, file); - foreach (var (path, file) in FileSwaps) + foreach (var (path, file) in container.FileSwaps) redirections.TryAdd(path, file); - manipulations.UnionWith(Manipulations); + manipulations.UnionWith(container.Manipulations); } public string GetName() diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 00216b77..12dfcada 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -65,7 +65,7 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + => IModDataContainer.AddDataTo(this, redirections, manipulations); public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) => new(null!, null!) diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 499ab192..ba91e271 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -63,7 +63,7 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + => IModDataContainer.AddDataTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); From 72db023804f1d7e529b2bca49e5293b884469432 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 Apr 2024 17:58:32 +0200 Subject: [PATCH 062/865] Some cleanup. --- Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 6 +- Penumbra/Mods/Groups/SingleModGroup.cs | 6 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 4 +- Penumbra/Mods/Mod.cs | 2 +- Penumbra/Mods/ModCreator.cs | 40 +++++--- Penumbra/Mods/SubMods/DefaultSubMod.cs | 6 +- Penumbra/Mods/SubMods/IModDataContainer.cs | 105 ++------------------- Penumbra/Mods/SubMods/IModOption.cs | 21 +---- Penumbra/Mods/SubMods/MultiSubMod.cs | 10 +- Penumbra/Mods/SubMods/SingleSubMod.cs | 10 +- Penumbra/Mods/SubMods/SubModHelpers.cs | 105 +++++++++++++++++++++ 12 files changed, 163 insertions(+), 154 deletions(-) create mode 100644 Penumbra/Mods/SubMods/SubModHelpers.cs diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index ed81f42f..e87fc012 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -51,7 +51,7 @@ public readonly struct ModSaveGroup : ISavable if (_groupIdx >= 0) _group!.WriteJson(j, serializer); else - IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + SubModHelpers.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index f39f2e70..1c8c769c 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -54,7 +54,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup return OptionData.Count - 1; } - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) + public static MultiModGroup? Load(Mod mod, JObject json) { var ret = new MultiModGroup(mod) { @@ -140,10 +140,10 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup jWriter.WriteStartArray(); foreach (var option in OptionData) { - IModOption.WriteModOption(jWriter, option); + SubModHelpers.WriteModOption(jWriter, option); jWriter.WritePropertyName(nameof(option.Priority)); jWriter.WriteValue(option.Priority.Value); - IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 6011185a..11542968 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -52,7 +52,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public bool IsOption => OptionData.Count > 1; - public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) + public static SingleModGroup? Load(Mod mod, JObject json) { var options = json["Options"]; var ret = new SingleModGroup(mod) @@ -144,8 +144,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup jWriter.WriteStartArray(); foreach (var option in OptionData) { - IModOption.WriteModOption(jWriter, option); - IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubModHelpers.WriteModOption(jWriter, option); + SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index b66fec17..a02c8d68 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -251,7 +251,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Description = option.Description, }; if (option is IModDataContainer data) - IModDataContainer.Clone(data, newOption); + SubModHelpers.Clone(data, newOption); s.OptionData.Add(newOption); break; } @@ -265,7 +265,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, }; if (option is IModDataContainer data) - IModDataContainer.Clone(data, newOption); + SubModHelpers.Clone(data, newOption); m.OptionData.Add(newOption); break; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index fc84afcc..6f6eb8ce 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -78,7 +78,7 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - Default.AddDataTo(dictRedirections, setManips); + Default.AddTo(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index e8113ee1..0626bc9d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -115,7 +115,7 @@ public partial class ModCreator( try { var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); - IModDataContainer.Load(jObject, mod.Default, mod.ModPath); + SubModHelpers.LoadDataContainer(jObject, mod.Default, mod.ModPath); } catch (Exception e) { @@ -162,7 +162,7 @@ public partial class ModCreator( deleteList.AddRange(localDeleteList); } - IModDataContainer.DeleteDeleteList(deleteList, delete); + DeleteDeleteList(deleteList, delete); if (!changes) return; @@ -221,7 +221,7 @@ public partial class ModCreator( } } - IModDataContainer.DeleteDeleteList(deleteList, delete); + DeleteDeleteList(deleteList, delete); return (oldSize < option.Manipulations.Count, deleteList); } @@ -392,10 +392,8 @@ public partial class ModCreator( Penumbra.Log.Debug($"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName(oldPath)} after split."); using (var oldFile = File.CreateText(oldPath)) { - using var j = new JsonTextWriter(oldFile) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(oldFile); + j.Formatting = Formatting.Indented; json.WriteTo(j); } @@ -403,10 +401,8 @@ public partial class ModCreator( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName(newPath)} after split."); using (var newFile = File.CreateText(newPath)) { - using var j = new JsonTextWriter(newFile) - { - Formatting = Formatting.Indented, - }; + using var j = new JsonTextWriter(newFile); + j.Formatting = Formatting.Indented; clone.WriteTo(j); } @@ -436,8 +432,8 @@ public partial class ModCreator( var json = JObject.Parse(File.ReadAllText(file.FullName)); switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) { - case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx); - case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx); + case GroupType.Multi: return MultiModGroup.Load(mod, json); + case GroupType.Single: return SingleModGroup.Load(mod, json); } } catch (Exception e) @@ -446,5 +442,23 @@ public partial class ModCreator( } return null; + } + + internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) + { + if (!delete) + return; + + foreach (var file in deleteList) + { + try + { + File.Delete(file); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); + } + } } } diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 8b166505..8b00e2ae 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -22,9 +22,9 @@ public class DefaultSubMod(IMod mod) : IModDataContainer IModGroup? IModDataContainer.Group => null; - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => IModDataContainer.AddDataTo(this, redirections, manipulations); + public void AddTo(Dictionary redirections, HashSet manipulations) + => SubModHelpers.AddContainerTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (-1, 0); -} +} diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index c9420821..1e676816 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -1,5 +1,3 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -7,6 +5,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.SubMods; + public interface IModDataContainer { public IMod Mod { get; } @@ -16,116 +15,24 @@ public interface IModDataContainer public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } - public static void AddDataTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) - { - foreach (var (path, file) in container.Files) - redirections.TryAdd(path, file); - - foreach (var (path, file) in container.FileSwaps) - redirections.TryAdd(path, file); - manipulations.UnionWith(container.Manipulations); - } - public string GetName() => this switch { - IModOption o => o.FullName, + IModOption o => o.FullName, DefaultSubMod => DefaultSubMod.FullName, - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; public string GetFullName() => this switch { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", - _ => $"Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", }; - public static void Clone(IModDataContainer from, IModDataContainer to) - { - to.Files = new Dictionary(from.Files); - to.FileSwaps = new Dictionary(from.FileSwaps); - to.Manipulations = [.. from.Manipulations]; - } - public (int GroupIndex, int DataIndex) GetDataIndices(); - - public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) - { - data.Files.Clear(); - data.FileSwaps.Clear(); - data.Manipulations.Clear(); - - var files = (JObject?)json[nameof(Files)]; - if (files != null) - foreach (var property in files.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); - } - - var swaps = (JObject?)json[nameof(FileSwaps)]; - if (swaps != null) - foreach (var property in swaps.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); - } - - var manips = json[nameof(Manipulations)]; - if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - data.Manipulations.Add(s); - } - - public static void WriteModData(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) - { - j.WritePropertyName(nameof(data.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(data.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(data.Manipulations)); - serializer.Serialize(j, data.Manipulations); - j.WriteEndObject(); - } - - internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) - { - if (!delete) - return; - - foreach (var file in deleteList) - { - try - { - File.Delete(file); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); - } - } - } } public interface IModDataOption : IModOption, IModDataContainer; diff --git a/Penumbra/Mods/SubMods/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs index f1ce1d4c..83d632a0 100644 --- a/Penumbra/Mods/SubMods/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,27 +1,10 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - namespace Penumbra.Mods.SubMods; public interface IModOption { - public string Name { get; set; } - public string FullName { get; } + public string Name { get; set; } + public string FullName { get; } public string Description { get; set; } - public static void Load(JToken json, IModOption option) - { - option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; - option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; - } - public (int GroupIndex, int OptionIndex) GetOptionIndices(); - - public static void WriteModOption(JsonWriter j, IModOption option) - { - j.WritePropertyName(nameof(Name)); - j.WriteValue(option.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(option.Description); - } } diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 12dfcada..ccb787a5 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -35,8 +35,8 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption public MultiSubMod(Mod mod, MultiModGroup group, JToken json) : this(mod, group) { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); + SubModHelpers.LoadOptionData(json, this); + SubModHelpers.LoadDataContainer(json, this, mod.ModPath); Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; } @@ -48,7 +48,7 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption Description = Description, Priority = Priority, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } @@ -60,12 +60,12 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption Name = Name, Description = Description, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => IModDataContainer.AddDataTo(this, redirections, manipulations); + => SubModHelpers.AddContainerTo(this, redirections, manipulations); public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) => new(null!, null!) diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index ba91e271..e6740a47 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -33,8 +33,8 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption public SingleSubMod(Mod mod, SingleModGroup group, JToken json) : this(mod, group) { - IModOption.Load(json, this); - IModDataContainer.Load(json, this, mod.ModPath); + SubModHelpers.LoadOptionData(json, this); + SubModHelpers.LoadDataContainer(json, this, mod.ModPath); } public SingleSubMod Clone(Mod mod, SingleModGroup group) @@ -44,7 +44,7 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption Name = Name, Description = Description, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } @@ -57,13 +57,13 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption Description = Description, Priority = priority, }; - IModDataContainer.Clone(this, ret); + SubModHelpers.Clone(this, ret); return ret; } public void AddDataTo(Dictionary redirections, HashSet manipulations) - => IModDataContainer.AddDataTo(this, redirections, manipulations); + => SubModHelpers.AddContainerTo(this, redirections, manipulations); public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); diff --git a/Penumbra/Mods/SubMods/SubModHelpers.cs b/Penumbra/Mods/SubMods/SubModHelpers.cs new file mode 100644 index 00000000..9992b6e8 --- /dev/null +++ b/Penumbra/Mods/SubMods/SubModHelpers.cs @@ -0,0 +1,105 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.ItemSwap; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public static class SubModHelpers +{ + /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. + public static void AddContainerTo(IModDataContainer container, Dictionary redirections, + HashSet manipulations) + { + foreach (var (path, file) in container.Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in container.FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(container.Manipulations); + } + + /// Replace all data of with the data of . + public static void Clone(IModDataContainer from, IModDataContainer to) + { + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); + to.Manipulations = [.. from.Manipulations]; + } + + /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. + public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath) + { + data.Files.Clear(); + data.FileSwaps.Clear(); + data.Manipulations.Clear(); + + var files = (JObject?)json[nameof(data.Files)]; + if (files != null) + foreach (var property in files.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); + } + + var swaps = (JObject?)json[nameof(data.FileSwaps)]; + if (swaps != null) + foreach (var property in swaps.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); + } + + var manips = json[nameof(data.Manipulations)]; + if (manips != null) + foreach (var s in manips.Children().Select(c => c.ToObject()) + .Where(m => m.Validate())) + data.Manipulations.Add(s); + } + + /// Load the relevant data for a selectable option from a JToken of that option. + public static void LoadOptionData(JToken json, IModOption option) + { + option.Name = json[nameof(option.Name)]?.ToObject() ?? string.Empty; + option.Description = json[nameof(option.Description)]?.ToObject() ?? string.Empty; + } + + /// Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. + public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) + { + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); + j.WriteEndObject(); + } + + /// Write the data for a selectable mod option on a JsonWriter. + public static void WriteModOption(JsonWriter j, IModOption option) + { + j.WritePropertyName(nameof(option.Name)); + j.WriteValue(option.Name); + j.WritePropertyName(nameof(option.Description)); + j.WriteValue(option.Description); + } +} From 0fd14ffefc11475920e7a7f1cf00c70e8ba46e22 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 Apr 2024 18:14:21 +0200 Subject: [PATCH 063/865] More cleanup. --- Penumbra/Mods/SubMods/DefaultSubMod.cs | 7 ++- Penumbra/Mods/SubMods/IModDataContainer.cs | 21 +------ Penumbra/Mods/SubMods/MultiSubMod.cs | 64 ++++------------------ Penumbra/Mods/SubMods/OptionSubMod.cs | 57 +++++++++++++++++++ Penumbra/Mods/SubMods/SingleSubMod.cs | 64 ++++------------------ Penumbra/Mods/SubMods/SubModHelpers.cs | 5 +- 6 files changed, 89 insertions(+), 129 deletions(-) create mode 100644 Penumbra/Mods/SubMods/OptionSubMod.cs diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 8b00e2ae..1eddcef6 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -25,6 +24,12 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public void AddTo(Dictionary redirections, HashSet manipulations) => SubModHelpers.AddContainerTo(this, redirections, manipulations); + public string GetName() + => FullName; + + public string GetFullName() + => FullName; + public (int GroupIndex, int DataIndex) GetDataIndices() => (-1, 0); } diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 1e676816..a6ab491f 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -15,24 +15,7 @@ public interface IModDataContainer public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } - public string GetName() - => this switch - { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, - _ => $"Container {GetDataIndices().DataIndex + 1}", - }; - - public string GetFullName() - => this switch - { - IModOption o => o.FullName, - DefaultSubMod => DefaultSubMod.FullName, - _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", - _ => $"Container {GetDataIndices().DataIndex + 1}", - }; - + public string GetName(); + public string GetFullName(); public (int GroupIndex, int DataIndex) GetDataIndices(); } - -public interface IModDataOption : IModOption, IModDataContainer; diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index ccb787a5..c43d4b9e 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -1,37 +1,13 @@ -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.SubMods; - -public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption + +namespace Penumbra.Mods.SubMods; + +public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(mod, group) { - internal readonly Mod Mod = mod; - internal readonly MultiModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; public ModPriority Priority { get; set; } = ModPriority.Default; - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) : this(mod, group) { @@ -44,9 +20,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { var ret = new MultiSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, - Priority = Priority, + Priority = Priority, }; SubModHelpers.Clone(this, ret); @@ -57,36 +33,18 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { var ret = new SingleSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, }; SubModHelpers.Clone(this, ret); return ret; } - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); - public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) => new(null!, null!) { - Name = name, + Name = name, Description = description, - Priority = priority, + Priority = priority, }; - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} +} diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs new file mode 100644 index 00000000..79c50e51 --- /dev/null +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -0,0 +1,57 @@ +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + +public interface IModDataOption : IModDataContainer, IModOption; + +public abstract class OptionSubMod(Mod mod, T group) : IModDataOption + where T : IModGroup +{ + internal readonly Mod Mod = mod; + internal readonly IModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group!.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => SubModHelpers.AddContainerTo(this, redirections, manipulations); + + public string GetName() + => Name; + + public string GetFullName() + => FullName; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index e6740a47..5d68e401 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -1,37 +1,13 @@ -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.SubMods; - -public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption + +namespace Penumbra.Mods.SubMods; + +public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod(mod, singleGroup) { - internal readonly Mod Mod = mod; - internal readonly SingleModGroup Group = group; - - public string Name { get; set; } = "Option"; - - public string FullName - => $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - IMod IModDataContainer.Mod - => Mod; - - IModGroup IModDataContainer.Group - => Group; - - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; - - public SingleSubMod(Mod mod, SingleModGroup group, JToken json) - : this(mod, group) + public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json) + : this(mod, singleGroup) { SubModHelpers.LoadOptionData(json, this); SubModHelpers.LoadDataContainer(json, this, mod.ModPath); @@ -41,7 +17,7 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { var ret = new SingleSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, }; SubModHelpers.Clone(this, ret); @@ -53,30 +29,12 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { var ret = new MultiSubMod(mod, group) { - Name = Name, + Name = Name, Description = Description, - Priority = priority, + Priority = priority, }; SubModHelpers.Clone(this, ret); return ret; } - - public void AddDataTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); - - public (int GroupIndex, int DataIndex) GetDataIndices() - => (Group.GetIndex(), GetDataIndex()); - - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); - - private int GetDataIndex() - { - var dataIndex = Group.DataContainers.IndexOf(this); - if (dataIndex < 0) - throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); - - return dataIndex; - } -} +} diff --git a/Penumbra/Mods/SubMods/SubModHelpers.cs b/Penumbra/Mods/SubMods/SubModHelpers.cs index 9992b6e8..2a09fbc3 100644 --- a/Penumbra/Mods/SubMods/SubModHelpers.cs +++ b/Penumbra/Mods/SubMods/SubModHelpers.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.ItemSwap; using Penumbra.String.Classes; namespace Penumbra.Mods.SubMods; @@ -92,9 +91,9 @@ public static class SubModHelpers j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); j.WriteEndObject(); - } + } - /// Write the data for a selectable mod option on a JsonWriter. + /// Write the data for a selectable mod option on a JsonWriter. public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(option.Name)); From 06953c175dbb1e733612c02e8bf815ed82247d29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 25 Apr 2024 18:18:57 +0200 Subject: [PATCH 064/865] mooooore --- Penumbra/Mods/Groups/IModGroup.cs | 15 --------------- Penumbra/Mods/Groups/ModSaveGroup.cs | 15 +++++++++++++++ Penumbra/Mods/Groups/MultiModGroup.cs | 4 +++- Penumbra/Mods/Groups/SingleModGroup.cs | 4 +++- Penumbra/Mods/SubMods/DefaultSubMod.cs | 20 ++++++++++---------- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index dc5150cf..0cabc9f3 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -68,21 +68,6 @@ public interface IModGroup return true; } - public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) - { - jWriter.WriteStartObject(); - jWriter.WritePropertyName(nameof(group.Name)); - jWriter.WriteValue(group!.Name); - jWriter.WritePropertyName(nameof(group.Description)); - jWriter.WriteValue(group.Description); - jWriter.WritePropertyName(nameof(group.Priority)); - jWriter.WriteValue(group.Priority.Value); - jWriter.WritePropertyName(nameof(group.Type)); - jWriter.WriteValue(group.Type.ToString()); - jWriter.WritePropertyName(nameof(group.DefaultSettings)); - jWriter.WriteValue(group.DefaultSettings.Value); - } - public (int Redirections, int Swaps, int Manips) GetCounts(); public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index e87fc012..92ccb36e 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -54,4 +54,19 @@ public readonly struct ModSaveGroup : ISavable SubModHelpers.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); } + + public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) + { + jWriter.WriteStartObject(); + jWriter.WritePropertyName(nameof(group.Name)); + jWriter.WriteValue(group!.Name); + jWriter.WritePropertyName(nameof(group.Description)); + jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Priority)); + jWriter.WriteValue(group.Priority.Value); + jWriter.WritePropertyName(nameof(group.Type)); + jWriter.WriteValue(group.Type.ToString()); + jWriter.WritePropertyName(nameof(group.DefaultSettings)); + jWriter.WriteValue(group.DefaultSettings.Value); + } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 1c8c769c..7e900ef5 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -135,15 +135,17 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { - IModGroup.WriteJsonBase(jWriter, this); + ModSaveGroup.WriteJsonBase(jWriter, this); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) { + jWriter.WriteStartObject(); SubModHelpers.WriteModOption(jWriter, option); jWriter.WritePropertyName(nameof(option.Priority)); jWriter.WriteValue(option.Priority.Value); SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 11542968..6aa9160e 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -139,13 +139,15 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { - IModGroup.WriteJsonBase(jWriter, this); + ModSaveGroup.WriteJsonBase(jWriter, this); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) { + jWriter.WriteStartObject(); SubModHelpers.WriteModOption(jWriter, option); SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); } jWriter.WriteEndArray(); diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 1eddcef6..980b805d 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -1,19 +1,19 @@ -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Groups; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.SubMods; - +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.SubMods; + public class DefaultSubMod(IMod mod) : IModDataContainer { public const string FullName = "Default Option"; internal readonly IMod Mod = mod; - public Dictionary Files { get; set; } = []; - public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; IMod IModDataContainer.Mod => Mod; From a72be22d3b295250716408fa0d01976f44a6e7b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 10:56:06 +0200 Subject: [PATCH 065/865] Make sure HS image does not displace the settings entirely. --- Penumbra/UI/ModsTab/ModPanelHeader.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 05f47809..a8b393b1 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -18,6 +18,7 @@ public class ModPanelHeader : IDisposable private readonly IFontHandle _nameFont; private readonly CommunicatorService _communicator; + private float _lastPreSettingsHeight = 0; public ModPanelHeader(DalamudPluginInterface pi, CommunicatorService communicator) { @@ -32,6 +33,11 @@ public class ModPanelHeader : IDisposable /// public void Draw() { + var height = ImGui.GetContentRegionAvail().Y; + var maxHeight = 3 * height / 4; + using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers + ? ImRaii.Child("HeaderChild", new Vector2(ImGui.GetContentRegionAvail().X, maxHeight), false) + : null; using (ImRaii.Group()) { var offset = DrawModName(); @@ -40,6 +46,7 @@ public class ModPanelHeader : IDisposable } _communicator.PreSettingsTabBarDraw.Invoke(_mod.Identifier, ImGui.GetItemRectSize().X, _nameWidth); + _lastPreSettingsHeight = ImGui.GetCursorPosY(); } /// @@ -48,6 +55,7 @@ public class ModPanelHeader : IDisposable /// public void UpdateModData(Mod mod) { + _lastPreSettingsHeight = 0; _mod = mod; // Name var name = $" {mod.Name} "; From e40c4999b6d18deab2bf98a60cd9cf5d1ac2b198 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 10:56:36 +0200 Subject: [PATCH 066/865] Improve collection migration maybe. --- .../Collections/Manager/CollectionStorage.cs | 30 +++++++++++++++---- .../Manager/ModCollectionMigration.cs | 2 -- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 4e2fb7b7..1fe5b227 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -184,21 +184,39 @@ public class CollectionStorage : IReadOnlyList, IDisposable { if (version >= 2) { - File.Move(file.FullName, correctName, false); - Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", - NotificationType.Warning); + try + { + File.Move(file.FullName, correctName, false); + Penumbra.Messager.NotificationMessage( + $"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", + NotificationType.Warning); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage( + $"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}", + NotificationType.Warning); + } } else { _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); - File.Delete(file.FullName); - Penumbra.Log.Information($"Migrated collection {name} to Guid {id}."); + try + { + File.Move(file.FullName, file.FullName + ".bak", true); + Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file."); + } + catch (Exception ex) + { + Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}"); + } } } catch (Exception e) { Penumbra.Messager.NotificationMessage(e, - $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", NotificationType.Error); + $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", + NotificationType.Error); } _collections.Add(collection); diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index 89743aa2..fe61285d 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -1,8 +1,6 @@ -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; using Penumbra.Services; -using Penumbra.Util; namespace Penumbra.Collections.Manager; From 297be487b50066c4a79c4cff03af18fa452b0672 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 10:57:09 +0200 Subject: [PATCH 067/865] More cleanup on groups. --- .../Communication/PreSettingsTabBarDraw.cs | 3 +- Penumbra/Mods/Groups/IModGroup.cs | 61 +++---------------- Penumbra/Mods/Groups/ModGroup.cs | 42 +++++++++++++ Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 6 +- Penumbra/Mods/Groups/SingleModGroup.cs | 6 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 42 +++++-------- Penumbra/Mods/Manager/ModStorage.cs | 6 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/MultiSubMod.cs | 8 +-- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/SubMods/SingleSubMod.cs | 8 +-- .../SubMods/{SubModHelpers.cs => SubMod.cs} | 19 +++++- 14 files changed, 107 insertions(+), 102 deletions(-) create mode 100644 Penumbra/Mods/Groups/ModGroup.cs rename Penumbra/Mods/SubMods/{SubModHelpers.cs => SubMod.cs} (87%) diff --git a/Penumbra/Communication/PreSettingsTabBarDraw.cs b/Penumbra/Communication/PreSettingsTabBarDraw.cs index 8614bbbe..e1d67297 100644 --- a/Penumbra/Communication/PreSettingsTabBarDraw.cs +++ b/Penumbra/Communication/PreSettingsTabBarDraw.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api.Api; +using Penumbra.Api.IpcSubscribers; namespace Penumbra.Communication; @@ -15,7 +16,7 @@ public sealed class PreSettingsTabBarDraw() : EventWrapper + /// Default = 0, } } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 0cabc9f3..b13799cd 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -16,22 +16,22 @@ public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; } + public string Description { get; set; } + public GroupType Type { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public int AddOption(Mod mod, string name, string description = ""); - public IReadOnlyList Options { get; } + public IReadOnlyList Options { get; } public IReadOnlyList DataContainers { get; } - public bool IsOption { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); - public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public bool MoveOption(int optionIdxFrom, int optionIdxTo); public int GetIndex(); @@ -42,46 +42,5 @@ public interface IModGroup public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null); - public bool ChangeOptionDescription(int optionIndex, string newDescription) - { - if (optionIndex < 0 || optionIndex >= Options.Count) - return false; - - var option = Options[optionIndex]; - if (option.Description == newDescription) - return false; - - option.Description = newDescription; - return true; - } - - public bool ChangeOptionName(int optionIndex, string newName) - { - if (optionIndex < 0 || optionIndex >= Options.Count) - return false; - - var option = Options[optionIndex]; - if (option.Name == newName) - return false; - - option.Name = newName; - return true; - } - public (int Redirections, int Swaps, int Manips) GetCounts(); - - public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) - { - var redirectionCount = 0; - var swapCount = 0; - var manipCount = 0; - foreach (var option in group.DataContainers) - { - redirectionCount += option.Files.Count; - swapCount += option.FileSwaps.Count; - manipCount += option.Manipulations.Count; - } - - return (redirectionCount, swapCount, manipCount); - } } diff --git a/Penumbra/Mods/Groups/ModGroup.cs b/Penumbra/Mods/Groups/ModGroup.cs new file mode 100644 index 00000000..da302714 --- /dev/null +++ b/Penumbra/Mods/Groups/ModGroup.cs @@ -0,0 +1,42 @@ +using Penumbra.Api.Enums; +using Penumbra.Mods.Settings; + +namespace Penumbra.Mods.Groups; + +public static class ModGroup +{ + public static IModGroup Create(Mod mod, GroupType type, string name) + { + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + return type switch + { + GroupType.Single => new SingleModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, + GroupType.Multi => new MultiModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), + }; + } + + + public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) + { + var redirectionCount = 0; + var swapCount = 0; + var manipCount = 0; + foreach (var option in group.DataContainers) + { + redirectionCount += option.Files.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; + } + + return (redirectionCount, swapCount, manipCount); + } +} diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 92ccb36e..332879cb 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -51,7 +51,7 @@ public readonly struct ModSaveGroup : ISavable if (_groupIdx >= 0) _group!.WriteJson(j, serializer); else - SubModHelpers.WriteModContainer(j, serializer, _defaultMod!, _basePath); + SubMod.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7e900ef5..6b352f66 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -141,10 +141,10 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup foreach (var option in OptionData) { jWriter.WriteStartObject(); - SubModHelpers.WriteModOption(jWriter, option); + SubMod.WriteModOption(jWriter, option); jWriter.WritePropertyName(nameof(option.Priority)); jWriter.WriteValue(option.Priority.Value); - SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubMod.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); jWriter.WriteEndObject(); } @@ -153,7 +153,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } public (int Redirections, int Swaps, int Manips) GetCounts() - => IModGroup.GetCountsBase(this); + => ModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) => new(setting.Value & (1ul << OptionData.Count) - 1); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 6aa9160e..ac85e2bc 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -135,7 +135,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); public (int Redirections, int Swaps, int Manips) GetCounts() - => IModGroup.GetCountsBase(this); + => ModGroup.GetCountsBase(this); public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { @@ -145,8 +145,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup foreach (var option in OptionData) { jWriter.WriteStartObject(); - SubModHelpers.WriteModOption(jWriter, option); - SubModHelpers.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); + SubMod.WriteModOption(jWriter, option); + SubMod.WriteModContainer(jWriter, serializer, option, basePath ?? Mod.ModPath); jWriter.WriteEndObject(); } diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index a02c8d68..c6122ea8 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -68,7 +68,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return; saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - var _ = group switch + _ = group switch { SingleModGroup s => s.Name = newName, MultiModGroup m => m.Name = newName, @@ -85,21 +85,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!VerifyFileName(mod, null, newName, true)) return; - var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - - mod.Groups.Add(type == GroupType.Multi - ? new MultiModGroup(mod) - { - Name = newName, - Priority = maxPriority, - } - : new SingleModGroup(mod) - { - Name = newName, - Priority = maxPriority, - }); - saveService.Save(saveType, new ModSaveGroup(mod, mod.Groups.Count - 1, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); + var idx = mod.Groups.Count; + var group = ModGroup.Create(mod, type, newName); + mod.Groups.Add(group); + saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1); } /// Add a new mod, empty option group of the given type and name if it does not exist already. @@ -142,12 +132,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (group.Description == newDescription) return; - var _ = group switch - { - SingleModGroup s => s.Description = newDescription, - MultiModGroup m => m.Description = newDescription, - _ => newDescription, - }; + group.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); } @@ -155,9 +140,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Change the description of the given option. public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - if (!mod.Groups[groupIdx].ChangeOptionDescription(optionIdx, newDescription)) + var option = mod.Groups[groupIdx].Options[optionIdx]; + if (option.Description == newDescription) return; + option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -193,9 +180,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Rename the given option. public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - if (!mod.Groups[groupIdx].ChangeOptionName(optionIdx, newName)) + var option = mod.Groups[groupIdx].Options[optionIdx]; + if (option.Name == newName) return; + option.Name = newName; + saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -251,7 +241,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Description = option.Description, }; if (option is IModDataContainer data) - SubModHelpers.Clone(data, newOption); + SubMod.Clone(data, newOption); s.OptionData.Add(newOption); break; } @@ -265,7 +255,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, }; if (option is IModDataContainer data) - SubModHelpers.Clone(data, newOption); + SubMod.Clone(data, newOption); m.OptionData.Add(newOption); break; } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 65b8ddd9..acb2c1ab 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -3,17 +3,13 @@ using OtterGui.Widgets; namespace Penumbra.Mods.Manager; -public class ModCombo : FilterComboCache +public class ModCombo(Func> generator) : FilterComboCache(generator, MouseWheelType.None, Penumbra.Log) { protected override bool IsVisible(int globalIndex, LowerString filter) => Items[globalIndex].Name.Contains(filter); protected override string ToString(Mod obj) => obj.Name.Text; - - public ModCombo(Func> generator) - : base(generator, MouseWheelType.None, Penumbra.Log) - { } } public class ModStorage : IReadOnlyList diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0626bc9d..40f943c8 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -115,7 +115,7 @@ public partial class ModCreator( try { var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); - SubModHelpers.LoadDataContainer(jObject, mod.Default, mod.ModPath); + SubMod.LoadDataContainer(jObject, mod.Default, mod.ModPath); } catch (Exception e) { diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 980b805d..1a234879 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -22,7 +22,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer => null; public void AddTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); + => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() => FullName; diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index c43d4b9e..3bcaffab 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -11,8 +11,8 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod() ?? ModPriority.Default; } @@ -24,7 +24,7 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(Mod mod, T group) : IModDataOption public HashSet Manipulations { get; set; } = []; public void AddDataTo(Dictionary redirections, HashSet manipulations) - => SubModHelpers.AddContainerTo(this, redirections, manipulations); + => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() => Name; diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 5d68e401..98c56151 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -9,8 +9,8 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod group switch + { + SingleModGroup single => new SingleSubMod(group.Mod, single) + { + Name = name, + Description = description, + }, + MultiModGroup multi => new MultiSubMod(group.Mod, multi) + { + Name = name, + Description = description, + }, + _ => throw new ArgumentOutOfRangeException(nameof(group)), + }; + /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. public static void AddContainerTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) From 616db0dcc3a43fae6faeadde80260fa625d71cc9 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 26 Apr 2024 21:23:31 +1000 Subject: [PATCH 068/865] Add mesh vertex element readout --- Penumbra/Import/Models/Export/MeshExporter.cs | 1 - .../UI/AdvancedWindow/ModEditWindow.Models.cs | 44 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 19b06d55..219a046e 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -1,5 +1,4 @@ using System.Collections.Immutable; -using Lumina.Data.Parsing; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 39924021..03b5169a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using ImGuiNET; +using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; @@ -8,7 +9,6 @@ using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; -using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -421,6 +421,14 @@ public partial class ModEditWindow var file = tab.Mdl; var mesh = file.Meshes[meshIndex]; + // Vertex elements + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Vertex Elements"); + + ImGui.TableNextColumn(); + DrawVertexElementDetails(file.VertexDeclarations[meshIndex].VertexElements); + // Mesh material ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -436,6 +444,40 @@ public partial class ModEditWindow return ret; } + private static void DrawVertexElementDetails(MdlStructs.VertexElement[] vertexElements) + { + using var node = ImRaii.TreeNode($"Click to expand"); + if (!node) + return; + + var flags = ImGuiTableFlags.SizingFixedFit + | ImGuiTableFlags.RowBg + | ImGuiTableFlags.Borders + | ImGuiTableFlags.NoHostExtendX; + using var table = ImRaii.Table(string.Empty, 4, flags); + if (!table) + return; + + ImGui.TableSetupColumn("Usage"); + ImGui.TableSetupColumn("Type"); + ImGui.TableSetupColumn("Stream"); + ImGui.TableSetupColumn("Offset"); + + ImGui.TableHeadersRow(); + + foreach (var element in vertexElements) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{(MdlFile.VertexUsage)element.Usage}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{(MdlFile.VertexType)element.Type}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{element.Stream}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{element.Offset}"); + } + } + private static bool DrawMaterialCombo(MdlTab tab, int meshIndex, bool disabled) { var mesh = tab.Mdl.Meshes[meshIndex]; From 1e5ed1c41450ba0d8b3b5dd85a8571ee49b6dd71 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Apr 2024 18:43:45 +0200 Subject: [PATCH 069/865] Now that was a lot of work. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 3 +- .../Cache/CollectionCacheManager.cs | 4 +- .../Collections/Manager/CollectionStorage.cs | 6 +- Penumbra/Communication/ModOptionChanged.cs | 17 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 13 +- Penumbra/Mods/Editor/ModEditor.cs | 46 +- Penumbra/Mods/Editor/ModFileEditor.cs | 9 +- Penumbra/Mods/Editor/ModMerger.cs | 69 +-- Penumbra/Mods/Editor/ModMetaEditor.cs | 4 +- Penumbra/Mods/Editor/ModNormalizer.cs | 8 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 4 +- Penumbra/Mods/Groups/IModGroup.cs | 11 +- Penumbra/Mods/Groups/ImcModGroup.cs | 158 +++++++ Penumbra/Mods/Groups/ModGroup.cs | 15 + Penumbra/Mods/Groups/ModSaveGroup.cs | 65 ++- Penumbra/Mods/Groups/MultiModGroup.cs | 78 ++-- Penumbra/Mods/Groups/SingleModGroup.cs | 91 ++-- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 11 +- Penumbra/Mods/Manager/ImcModGroupEditor.cs | 38 ++ Penumbra/Mods/Manager/ModCacheManager.cs | 4 +- Penumbra/Mods/Manager/ModGroupEditor.cs | 289 +++++++++++++ Penumbra/Mods/Manager/ModManager.cs | 9 +- Penumbra/Mods/Manager/ModMigration.cs | 16 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 392 +++--------------- Penumbra/Mods/Manager/MultiModGroupEditor.cs | 84 ++++ Penumbra/Mods/Manager/SingleModGroupEditor.cs | 57 +++ Penumbra/Mods/ModCreator.cs | 20 +- Penumbra/Mods/Settings/ModSettings.cs | 44 +- Penumbra/Mods/Settings/Setting.cs | 28 ++ Penumbra/Mods/SubMods/IModOption.cs | 7 +- Penumbra/Mods/SubMods/ImcSubMod.cs | 32 ++ Penumbra/Mods/SubMods/MultiSubMod.cs | 20 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 47 ++- Penumbra/Mods/SubMods/SingleSubMod.cs | 16 +- Penumbra/Mods/SubMods/SubMod.cs | 32 +- Penumbra/Penumbra.csproj | 10 +- Penumbra/Services/SaveService.cs | 9 +- Penumbra/Services/StaticServiceManager.cs | 1 - Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 35 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 132 +++--- 44 files changed, 1182 insertions(+), 766 deletions(-) create mode 100644 Penumbra/Mods/Groups/ImcModGroup.cs create mode 100644 Penumbra/Mods/Manager/ImcModGroupEditor.cs create mode 100644 Penumbra/Mods/Manager/ModGroupEditor.cs create mode 100644 Penumbra/Mods/Manager/MultiModGroupEditor.cs create mode 100644 Penumbra/Mods/Manager/SingleModGroupEditor.cs create mode 100644 Penumbra/Mods/SubMods/ImcSubMod.cs diff --git a/Penumbra.Api b/Penumbra.Api index 590629df..69d106b4 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a +Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 039fbfa9..bfd134bb 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -11,6 +11,7 @@ using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Api.Api; @@ -254,7 +255,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) { switch (type) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c1296414..9c104cef 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -7,8 +7,10 @@ using Penumbra.Communication; using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; @@ -257,7 +259,7 @@ public class CollectionCacheManager : IDisposable } /// Prepare Changes by removing mods from caches with collections or add or reload mods. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) { if (type is ModOptionChangeType.PrepareChange) { diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 1fe5b227..bfae2dc0 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -4,8 +4,10 @@ using OtterGui.Classes; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Collections.Manager; @@ -290,7 +292,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable } /// Save all collections where the mod has settings and the change requires saving. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) { type.HandlingInfo(out var requiresSaving, out _, out _); if (!requiresSaving) @@ -298,7 +300,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this) { - if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index 0df58b5f..a20592ec 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,8 +1,10 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; +using static Penumbra.Communication.ModOptionChanged; namespace Penumbra.Communication; @@ -11,22 +13,23 @@ namespace Penumbra.Communication; /// /// Parameter is the type option change. /// Parameter is the changed mod. -/// Parameter is the index of the changed group inside the mod. -/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. -/// Parameter is the index of the group an option was moved to. +/// Parameter is the changed group inside the mod. +/// Parameter is the changed option inside the group or null if it does not concern a specific option. +/// Parameter is the changed data container inside the group or null if it does not concern a specific data container. +/// Parameter is the index of the group or option moved or deleted from. /// public sealed class ModOptionChanged() - : EventWrapper(nameof(ModOptionChanged)) + : EventWrapper(nameof(ModOptionChanged)) { public enum Priority { - /// + /// Api = int.MinValue, /// CollectionCacheManager = -100, - /// + /// ModCacheManager = 0, /// diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index eb6d0b0c..2b45ecbe 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -205,7 +205,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default)); + options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 31aacbe1..84a832a2 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -60,7 +60,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager) { - ModEditor.ApplyToAllOptions(mod, HandleSubMod); + ModEditor.ApplyToAllContainers(mod, HandleSubMod); try { @@ -73,7 +73,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -82,14 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; if (useModManager) - { - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync); - } + modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); else - { - subMod.Files = dict; - saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - } + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index e1c5962f..37524da1 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -29,8 +29,8 @@ public class ModEditor( public int GroupIdx { get; private set; } public int DataIdx { get; private set; } - public IModGroup? Group { get; private set; } - public IModDataContainer? Option { get; private set; } + public IModGroup? Group { get; private set; } + public IModDataContainer? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); @@ -63,10 +63,10 @@ public class ModEditor( { if (groupIdx == -1 && dataIdx == 0) { - Group = null; - Option = Mod.Default; - GroupIdx = groupIdx; - DataIdx = dataIdx; + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } @@ -75,18 +75,18 @@ public class ModEditor( Group = Mod.Groups[groupIdx]; if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count) { - Option = Group.DataContainers[dataIdx]; - GroupIdx = groupIdx; - DataIdx = dataIdx; + Option = Group.DataContainers[dataIdx]; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } } } - Group = null; - Option = Mod?.Default; - GroupIdx = -1; - DataIdx = 0; + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + DataIdx = 0; if (message) Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } @@ -105,23 +105,11 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllContainers(Mod mod, Action action) { - action(mod.Default, -1, 0); - foreach (var (group, groupIdx) in mod.Groups.WithIndex()) - { - switch (group) - { - case SingleModGroup single: - for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) - action(single.OptionData[optionIdx], groupIdx, optionIdx); - break; - case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) - action(multi.OptionData[optionIdx], groupIdx, optionIdx); - break; - } - } + action(mod.Default); + foreach (var container in mod.Groups.SelectMany(g => g.DataContainers)) + action(container); } // Does not delete the base directory itself even if it is completely empty at the end. diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 00685c94..e2c0b726 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -24,8 +24,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - var (groupIdx, dataIdx) = option.GetDataIndices(); - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict); + modManager.OptionEditor.SetFiles(option, dict); files.UpdatePaths(mod, option); Changes = false; return num; @@ -40,15 +39,15 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths(Mod mod, IModDataContainer option) { - void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (newDict.Count != subMod.Files.Count) - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + modManager.OptionEditor.SetFiles(subMod, newDict); } - ModEditor.ApplyToAllOptions(mod, HandleSubMod); + ModEditor.ApplyToAllContainers(mod, HandleSubMod); files.ClearMissingFiles(); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 0f629bc7..3a6f4a81 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -16,7 +16,7 @@ public class ModMerger : IDisposable { private readonly Configuration _config; private readonly CommunicatorService _communicator; - private readonly ModOptionEditor _editor; + private readonly ModGroupEditor _editor; private readonly ModFileSystemSelector _selector; private readonly DuplicateManager _duplicates; private readonly ModManager _mods; @@ -32,14 +32,14 @@ public class ModMerger : IDisposable private readonly Dictionary _fileToFile = []; private readonly HashSet _createdDirectories = []; private readonly HashSet _createdGroups = []; - private readonly HashSet _createdOptions = []; + private readonly HashSet _createdOptions = []; public readonly HashSet SelectedOptions = []; public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } - public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { _editor = editor; @@ -100,22 +100,23 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); if (groupCreated) _createdGroups.Add(groupIdx); - if (group.Type != originalGroup.Type) - ((List)Warnings).Add( - $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); + if (group == null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); foreach (var originalOption in group.DataContainers) { - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName()); + var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); if (optionCreated) { - _createdOptions.Add((IModDataOption)option); - MergeIntoOption([originalOption], (IModDataOption)option, false); + _createdOptions.Add(option!); + // #TODO DataContainer <> Option. + MergeIntoOption([originalOption], (IModDataContainer)option!, false); } else { throw new Exception( - $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option.FullName} already existed."); + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); } } } @@ -138,9 +139,9 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); + var (option, _, optionCreated) = _editor.FindOrAddOption(group!, optionName, SaveType.None); if (optionCreated) - _createdOptions.Add((IModDataOption)option); + _createdOptions.Add(option!); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -148,7 +149,8 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); + // #TODO DataContainer <> Option. + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true); } private void MergeIntoOption(IEnumerable mergeOptions, IModDataContainer option, bool fromFileToFile) @@ -184,10 +186,9 @@ public class ModMerger : IDisposable } } - var (groupIdx, dataIdx) = option.GetDataIndices(); - _editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync); + _editor.SetFiles(option, redirections, SaveType.None); + _editor.SetFileSwaps(option, swaps, SaveType.None); + _editor.SetManipulations(option, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -261,30 +262,31 @@ public class ModMerger : IDisposable if (mods.Count == 1) { var files = CopySubModFiles(mods[0], dir); - _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); - _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); + _editor.SetFiles(result.Default, files); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); + _editor.SetManipulations(result.Default, mods[0].Manipulations); } else { foreach (var originalOption in mods) { - if (originalOption.Group is not {} originalGroup) + if (originalOption.Group is not { } originalGroup) { var files = CopySubModFiles(mods[0], dir); - _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); - _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); + _editor.SetFiles(result.Default, files); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); + _editor.SetManipulations(result.Default, mods[0].Manipulations); } else { - var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName()); - var folder = Path.Combine(dir.FullName, group.Name, option.Name); + // 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); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.OptionSetFiles(result, groupIdx, optionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps); - _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); + _editor.SetFiles((IModDataContainer)option, files); + _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps); + _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations); } } } @@ -339,16 +341,15 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - var (groupIdx, optionIdx) = option.GetOptionIndices(); - _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); + _editor.DeleteOption(option); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } foreach (var group in _createdGroups) { var groupName = MergeToMod!.Groups[group]; - _editor.DeleteModGroup(MergeToMod!, group); - Penumbra.Log.Verbose($"[Merger] Removed option group {groupName}."); + _editor.DeleteModGroup(groupName); + Penumbra.Log.Verbose($"[Merger] Removed option group {groupName.Name}."); } foreach (var dir in _createdDirectories) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index dee700d5..2f7fd04c 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -145,12 +145,12 @@ public class ModMetaEditor(ModManager modManager) Split(currentOption.Manipulations); } - public void Apply(Mod mod, int groupIdx, int optionIdx) + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet()); Changes = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index e2088b32..437600c9 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -283,12 +283,12 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) switch (group) { case SingleModGroup single: - foreach (var (_, optionIdx) in single.OptionData.WithIndex()) - _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + foreach (var (option, optionIdx) in single.OptionData.WithIndex()) + _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); break; case MultiModGroup multi: - foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) - _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + foreach (var (option, optionIdx) in multi.OptionData.WithIndex()) + _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); break; } } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 3247cfdf..0250efae 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -17,12 +17,12 @@ public class ModSwapEditor(ModManager modManager) Changes = false; } - public void Apply(Mod mod, int groupIdx, int optionIdx) + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + modManager.OptionEditor.SetFileSwaps(container, _swaps); Changes = false; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index b13799cd..a268ba0f 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -9,7 +9,7 @@ namespace Penumbra.Mods.Groups; public interface ITexToolsGroup { - public IReadOnlyList OptionData { get; } + public IReadOnlyList OptionData { get; } } public interface IModGroup @@ -17,22 +17,19 @@ public interface IModGroup public const int MaxMultiOptions = 63; public Mod Mod { get; } - public string Name { get; } + public string Name { get; set; } public string Description { get; set; } public GroupType Type { get; } public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public FullPath? FindBestMatch(Utf8GamePath gamePath); + public IModOption? AddOption(string name, string description = ""); public IReadOnlyList Options { get; } public IReadOnlyList DataContainers { get; } public bool IsOption { get; } - public IModGroup Convert(GroupType type); - public bool MoveOption(int optionIdxFrom, int optionIdxTo); - public int GetIndex(); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs new file mode 100644 index 00000000..e233f82e --- /dev/null +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Groups; + +public class ImcModGroup(Mod mod) : IModGroup +{ + public const int DisabledIndex = 30; + public const int NumAttributes = 10; + + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A single IMC manipulation."; + + public GroupType Type + => GroupType.Imc; + + public ModPriority Priority { get; set; } = ModPriority.Default; + public Setting DefaultSettings { get; set; } = Setting.Zero; + + public PrimaryId PrimaryId; + public SecondaryId SecondaryId; + public ObjectType ObjectType; + public BodySlot BodySlot; + public EquipSlot EquipSlot; + public Variant Variant; + + public ImcEntry DefaultEntry; + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => null; + + private bool _canBeDisabled = false; + + public bool CanBeDisabled + { + get => _canBeDisabled; + set + { + _canBeDisabled = value; + if (!value) + DefaultSettings = FixSetting(DefaultSettings); + } + } + + public IModOption? AddOption(string name, string description = "") + { + uint fullMask = GetFullMask(); + var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask); + // All attributes handled. + if (firstUnset >= NumAttributes) + return null; + + var groupIdx = Mod.Groups.IndexOf(this); + if (groupIdx < 0) + return null; + + var subMod = new ImcSubMod(this) + { + Name = name, + Description = description, + AttributeIndex = firstUnset, + }; + OptionData.Add(subMod); + return subMod; + } + + public readonly List OptionData = []; + + public IReadOnlyList Options + => OptionData; + + public IReadOnlyList DataContainers + => []; + + public bool IsOption + => CanBeDisabled || OptionData.Count > 0; + + public int GetIndex() + => ModGroup.GetIndex(this); + + private ushort GetCurrentMask(Setting setting) + { + var mask = DefaultEntry.AttributeMask; + for (var i = 0; i < OptionData.Count; ++i) + { + if (!setting.HasFlag(i)) + continue; + + var option = OptionData[i]; + mask |= option.Attribute; + } + + return mask; + } + + private ushort GetFullMask() + => GetCurrentMask(Setting.AllBits(63)); + + private ImcManipulation GetManip(ushort mask) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, + DefaultEntry with { AttributeMask = mask }); + + + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + if (CanBeDisabled && setting.HasFlag(DisabledIndex)) + return; + + var mask = GetCurrentMask(setting); + var imc = GetManip(mask); + manipulations.Add(imc); + } + + public Setting FixSetting(Setting setting) + => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName(nameof(ObjectType)); + jWriter.WriteValue(ObjectType.ToString()); + jWriter.WritePropertyName(nameof(BodySlot)); + jWriter.WriteValue(BodySlot.ToString()); + jWriter.WritePropertyName(nameof(EquipSlot)); + jWriter.WriteValue(EquipSlot.ToString()); + jWriter.WritePropertyName(nameof(PrimaryId)); + jWriter.WriteValue(PrimaryId.Id); + jWriter.WritePropertyName(nameof(SecondaryId)); + jWriter.WriteValue(SecondaryId.Id); + jWriter.WritePropertyName(nameof(Variant)); + jWriter.WriteValue(Variant.Id); + jWriter.WritePropertyName(nameof(DefaultEntry)); + serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + jWriter.WritePropertyName(nameof(option.AttributeIndex)); + jWriter.WriteValue(option.AttributeIndex); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => (0, 0, 1); +} diff --git a/Penumbra/Mods/Groups/ModGroup.cs b/Penumbra/Mods/Groups/ModGroup.cs index da302714..8b55a035 100644 --- a/Penumbra/Mods/Groups/ModGroup.cs +++ b/Penumbra/Mods/Groups/ModGroup.cs @@ -5,6 +5,7 @@ namespace Penumbra.Mods.Groups; public static class ModGroup { + /// Create a new mod group based on the given type. public static IModGroup Create(Mod mod, GroupType type, string name) { var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; @@ -20,6 +21,11 @@ public static class ModGroup Name = name, Priority = maxPriority, }, + GroupType.Imc => new ImcModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; } @@ -38,5 +44,14 @@ public static class ModGroup } return (redirectionCount, swapCount, manipCount); + } + + public static int GetIndex(IModGroup group) + { + var groupIndex = group.Mod.Groups.IndexOf(group); + if (groupIndex < 0) + throw new Exception($"Mod {group.Mod.Name} from Group {group.Name} does not contain this group."); + + return groupIndex; } } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 332879cb..efdcde09 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using Penumbra.Mods.SubMods; -using Penumbra.Services; - -namespace Penumbra.Mods.Groups; - +using Newtonsoft.Json; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Groups; + public readonly struct ModSaveGroup : ISavable { private readonly DirectoryInfo _basePath; @@ -12,25 +12,21 @@ public readonly struct ModSaveGroup : ISavable private readonly DefaultSubMod? _defaultMod; private readonly bool _onlyAscii; - public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) - { - _basePath = mod.ModPath; - _groupIdx = groupIdx; - if (_groupIdx < 0) - _defaultMod = mod.Default; - else - _group = mod.Groups[_groupIdx]; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) + private ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii) { _basePath = basePath; _group = group; - _groupIdx = groupIdx; + _groupIdx = groupIndex; _onlyAscii = onlyAscii; } + public static ModSaveGroup WithoutMod(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii) + => new(basePath, group, groupIndex, onlyAscii); + + public ModSaveGroup(IModGroup group, bool onlyAscii) + : this(group.Mod.ModPath, group, group.GetIndex(), onlyAscii) + { } + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) { _basePath = basePath; @@ -39,6 +35,33 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } + public ModSaveGroup(DirectoryInfo basePath, IModDataContainer container, bool onlyAscii) + { + _basePath = basePath; + _defaultMod = container as DefaultSubMod; + _onlyAscii = onlyAscii; + if (_defaultMod == null) + { + _groupIdx = -1; + _group = null; + } + else + { + _group = container.Group!; + _groupIdx = _group.GetIndex(); + } + } + + public ModSaveGroup(IModDataContainer container, bool onlyAscii) + { + _basePath = (container.Mod as Mod)?.ModPath + ?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen. + _defaultMod = null; + _onlyAscii = onlyAscii; + _group = container.Group!; + _groupIdx = _group.GetIndex(); + } + public string ToFilename(FilenameService fileNames) => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); @@ -59,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable { jWriter.WriteStartObject(); jWriter.WritePropertyName(nameof(group.Name)); - jWriter.WriteValue(group!.Name); + jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); jWriter.WriteValue(group.Description); jWriter.WritePropertyName(nameof(group.Priority)); @@ -69,4 +92,4 @@ public readonly struct ModSaveGroup : ISavable jWriter.WritePropertyName(nameof(group.DefaultSettings)); jWriter.WriteValue(group.DefaultSettings.Value); } -} +} diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 6b352f66..a0034be0 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -18,11 +17,11 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; public IReadOnlyList Options @@ -39,28 +38,28 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public int AddOption(Mod mod, string name, string description = "") + public IModOption? AddOption(string name, string description = "") { - var groupIdx = mod.Groups.IndexOf(this); + var groupIdx = Mod.Groups.IndexOf(this); if (groupIdx < 0) - return -1; + return null; - var subMod = new MultiSubMod(mod, this) + var subMod = new MultiSubMod(this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); - return OptionData.Count - 1; + return subMod; } public static MultiModGroup? Load(Mod mod, JObject json) { var ret = new MultiModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -78,7 +77,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup break; } - var subMod = new MultiSubMod(mod, ret, child); + var subMod = new MultiSubMod(ret, child); ret.OptionData.Add(subMod); } @@ -87,42 +86,21 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup return ret; } - public IModGroup Convert(GroupType type) + public SingleModGroup ConvertToSingle() { - switch (type) + var single = new SingleModGroup(Mod) { - case GroupType.Multi: return this; - case GroupType.Single: - var single = new SingleModGroup(Mod) - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), - }; - single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); - return single; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - } - - public bool MoveOption(int optionIdxFrom, int optionIdxTo) - { - if (!OptionData.Move(optionIdxFrom, optionIdxTo)) - return false; - - DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); - return true; + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), + }; + single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single))); + return single; } public int GetIndex() - { - var groupIndex = Mod.Groups.IndexOf(this); - if (groupIndex < 0) - throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); - - return groupIndex; - } + => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { @@ -156,15 +134,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => ModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) - => new(setting.Value & (1ul << OptionData.Count) - 1); + => new(setting.Value & ((1ul << OptionData.Count) - 1)); /// Create a group without a mod only for saving it in the creator. - internal static MultiModGroup CreateForSaving(string name) + internal static MultiModGroup WithoutMod(string name) => new(null!) { Name = name, }; - IReadOnlyList ITexToolsGroup.OptionData + IReadOnlyList ITexToolsGroup.OptionData => OptionData; } diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index ac85e2bc..0776c2af 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -16,31 +15,28 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; - IReadOnlyList ITexToolsGroup.OptionData - => OptionData; - public FullPath? FindBestMatch(Utf8GamePath gamePath) => OptionData .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public int AddOption(Mod mod, string name, string description = "") + public IModOption AddOption(string name, string description = "") { - var subMod = new SingleSubMod(mod, this) + var subMod = new SingleSubMod(this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); - return OptionData.Count - 1; + return subMod; } public IReadOnlyList Options @@ -57,9 +53,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup var options = json["Options"]; var ret = new SingleModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -68,7 +64,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SingleSubMod(mod, ret, child); + var subMod = new SingleSubMod(ret, child); ret.OptionData.Add(subMod); } @@ -76,57 +72,21 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup return ret; } - public IModGroup Convert(GroupType type) + public MultiModGroup ConvertToMulti() { - switch (type) + var multi = new MultiModGroup(Mod) { - case GroupType.Single: return this; - case GroupType.Multi: - var multi = new MultiModGroup(Mod) - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = Setting.Multi((int)DefaultSettings.Value), - }; - multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); - return multi; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = Setting.Multi((int)DefaultSettings.Value), + }; + multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i)))); + return multi; } - public bool MoveOption(int optionIdxFrom, int optionIdxTo) - { - if (!OptionData.Move(optionIdxFrom, optionIdxTo)) - return false; - - var currentIndex = DefaultSettings.AsIndex; - // Update default settings with the move. - if (currentIndex == optionIdxFrom) - { - DefaultSettings = Setting.Single(optionIdxTo); - } - else if (optionIdxFrom < optionIdxTo) - { - if (currentIndex > optionIdxFrom && currentIndex <= optionIdxTo) - DefaultSettings = Setting.Single(currentIndex - 1); - } - else if (currentIndex < optionIdxFrom && currentIndex >= optionIdxTo) - { - DefaultSettings = Setting.Single(currentIndex + 1); - } - - return true; - } - - public int GetIndex() - { - var groupIndex = Mod.Groups.IndexOf(this); - if (groupIndex < 0) - throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); - - return groupIndex; - } + public int GetIndex() + => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); @@ -160,4 +120,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = name, }; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 1545811e..449405a0 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -8,6 +9,7 @@ using Penumbra.Meta; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.ItemSwap; @@ -40,8 +42,7 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod(ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, - int groupIndex = -1, int optionIndex = 0) + public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null) { var convertedManips = new HashSet(Swaps.Count); var convertedFiles = new Dictionary(Swaps.Count); @@ -80,9 +81,9 @@ public class ItemSwapContainer } } - manager.OptionEditor.OptionSetFiles(mod, groupIndex, optionIndex, convertedFiles); - manager.OptionEditor.OptionSetFileSwaps(mod, groupIndex, optionIndex, convertedSwaps); - manager.OptionEditor.OptionSetManipulations(mod, groupIndex, optionIndex, convertedManips); + manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None); + manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None); + manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync); return true; } catch (Exception e) diff --git a/Penumbra/Mods/Manager/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/ImcModGroupEditor.cs new file mode 100644 index 00000000..4e2b2194 --- /dev/null +++ b/Penumbra/Mods/Manager/ImcModGroupEditor.cs @@ -0,0 +1,38 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) + => null; + + protected override void RemoveOption(ImcModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.FixSetting(group.DefaultSettings); + } + + protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 3ff1a333..0669696f 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,6 +2,8 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -103,7 +105,7 @@ public class ModCacheManager : IDisposable } } - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int fromIdx) { switch (type) { diff --git a/Penumbra/Mods/Manager/ModGroupEditor.cs b/Penumbra/Mods/Manager/ModGroupEditor.cs new file mode 100644 index 00000000..9f41fa6f --- /dev/null +++ b/Penumbra/Mods/Manager/ModGroupEditor.cs @@ -0,0 +1,289 @@ +using System.Text.RegularExpressions; +using Dalamud.Interface.Internal.Notifications; +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.Util; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; + +namespace Penumbra.Mods.Manager; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + GroupMoved, + GroupTypeChanged, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionMoved, + OptionFilesChanged, + OptionFilesAdded, + OptionSwapsChanged, + OptionMetaChanged, + DisplayChange, + PrepareChange, + DefaultOptionChanged, +} + +public class ModGroupEditor( + SingleModGroupEditor singleEditor, + MultiModGroupEditor multiEditor, + ImcModGroupEditor imcEditor, + CommunicatorService Communicator, + SaveService SaveService, + Configuration Config) : IService +{ + public SingleModGroupEditor SingleEditor + => singleEditor; + + public MultiModGroupEditor MultiEditor + => multiEditor; + + public ImcModGroupEditor ImcEditor + => imcEditor; + + /// Change the settings stored as default options in a mod. + public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption) + { + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); + } + + /// Rename an option group if possible. + public void RenameModGroup(IModGroup group, string newName) + { + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) + return; + + SaveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + group.Name = newName; + SaveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); + } + + /// Delete a given option group. Fires an event to prepare before actually deleting. + public void DeleteModGroup(IModGroup group) + { + var mod = group.Mod; + var idx = group.GetIndex(); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); + mod.Groups.RemoveAt(idx); + SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); + } + + /// Move the index of a given option group. + public void MoveModGroup(IModGroup group, int groupIdxTo) + { + var mod = group.Mod; + var idxFrom = group.GetIndex(); + if (!mod.Groups.Move(idxFrom, groupIdxTo)) + return; + + SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); + } + + /// Change the internal priority of the given option group. + public void ChangeGroupPriority(IModGroup group, ModPriority newPriority) + { + if (group.Priority == newPriority) + return; + + group.Priority = newPriority; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); + } + + /// Change the description of the given option group. + public void ChangeGroupDescription(IModGroup group, string newDescription) + { + if (group.Description == newDescription) + return; + + group.Description = newDescription; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); + } + + /// Rename the given option. + public void RenameOption(IModOption option, string newName) + { + if (option.Name == newName) + return; + + option.Name = newName; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + } + + /// Change the description of the given option. + public void ChangeOptionDescription(IModOption option, string newDescription) + { + if (option.Description == newDescription) + return; + + option.Description = newDescription; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + } + + /// Set the meta manipulations for a given option. Replaces existing manipulations. + public void SetManipulations(IModDataContainer subMod, HashSet manipulations, SaveType saveType = SaveType.Queue) + { + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.Manipulations.SetTo(manipulations); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Set the file redirections for a given option. Replaces existing redirections. + public void SetFiles(IModDataContainer subMod, IReadOnlyDictionary replacements, SaveType saveType = SaveType.Queue) + { + if (subMod.Files.SetEquals(replacements)) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.Files.SetTo(replacements); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. + public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) + { + var oldCount = subMod.Files.Count; + subMod.Files.AddFrom(additions); + if (oldCount != subMod.Files.Count) + { + SaveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + } + + /// Set the file swaps for a given option. Replaces existing swaps. + public void SetFileSwaps(IModDataContainer subMod, IReadOnlyDictionary swaps, SaveType saveType = SaveType.Queue) + { + if (subMod.FileSwaps.SetEquals(swaps)) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.FileSwaps.SetTo(swaps); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Verify that a new option group name is unique in this mod. + public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) + { + var path = newName.RemoveInvalidPathSymbols(); + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) + return true; + + if (message) + Penumbra.Messager.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + NotificationType.Warning, false); + + return false; + } + + public void DeleteOption(IModOption option) + { + switch (option) + { + case SingleSubMod s: + SingleEditor.DeleteOption(s); + return; + case MultiSubMod m: + MultiEditor.DeleteOption(m); + return; + case ImcSubMod i: + ImcEditor.DeleteOption(i); + return; + } + } + + public IModOption? AddOption(IModGroup group, IModOption option) + => group switch + { + SingleModGroup s => SingleEditor.AddOption(s, option), + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, + }; + + public IModOption? AddOption(IModGroup group, string newName) + => group switch + { + SingleModGroup s => SingleEditor.AddOption(s, newName), + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, + }; + + public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + => type switch + { + GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + _ => null, + }; + + public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) + => type switch + { + GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), + }; + + public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) + => group switch + { + SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), + }; + + public void MoveOption(IModOption option, int toIdx) + { + switch (option) + { + case SingleSubMod s: + SingleEditor.MoveOption(s, toIdx); + return; + case MultiSubMod m: + MultiEditor.MoveOption(m, toIdx); + return; + case ImcSubMod i: + ImcEditor.MoveOption(i, toIdx); + return; + } + } +} diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index d912e292..7a266a31 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,4 +1,3 @@ -using System.Security.AccessControl; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -32,14 +31,14 @@ public sealed class ModManager : ModStorage, IDisposable private readonly Configuration _config; private readonly CommunicatorService _communicator; - public readonly ModCreator Creator; - public readonly ModDataEditor DataEditor; - public readonly ModOptionEditor OptionEditor; + public readonly ModCreator Creator; + public readonly ModDataEditor DataEditor; + public readonly ModGroupEditor OptionEditor; public DirectoryInfo BasePath { get; private set; } = null!; public bool Valid { get; private set; } - public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor, + public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModGroupEditor optionEditor, ModCreator creator) { _config = config; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index f160d5bd..c7eb7cc5 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -83,8 +83,8 @@ public static partial class ModMigration mod.Default.FileSwaps.Add(gamePath, swapPath); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); - foreach (var (_, index) in mod.Groups.WithIndex()) - saveService.ImmediateSave(new ModSaveGroup(mod, index, creator.Config.ReplaceNonAsciiOnImport)); + foreach (var group in mod.Groups) + saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) @@ -112,7 +112,7 @@ public static partial class ModMigration } fileVersion = 1; - saveService.ImmediateSave(new ModSaveGroup(mod, -1, creator.Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default, creator.Config.ReplaceNonAsciiOnImport)); return true; } @@ -176,7 +176,7 @@ public static partial class ModMigration private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SingleSubMod(mod, group) + var subMod = new SingleSubMod(group) { Name = option.OptionName, Description = option.OptionDesc, @@ -189,7 +189,7 @@ public static partial class ModMigration private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, ModPriority priority, HashSet seenMetaFiles) { - var subMod = new MultiSubMod(mod, group) + var subMod = new MultiSubMod(group) { Name = option.OptionName, Description = option.OptionDesc, @@ -219,7 +219,7 @@ public static partial class ModMigration [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public GroupType SelectionType = GroupType.Single; - public List Options = new(); + public List Options = []; public OptionGroupV0() { } @@ -236,12 +236,12 @@ public static partial class ModMigration var token = JToken.Load(reader); if (token.Type == JTokenType.Array) - return token.ToObject>() ?? new HashSet(); + return token.ToObject>() ?? []; var tmp = token.ToObject(); return tmp != null ? new HashSet { tmp } - : new HashSet(); + : []; } public override bool CanWrite diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index c6122ea8..7370a933 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,384 +1,122 @@ -using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Mods.Manager; -public enum ModOptionChangeType +public abstract class ModOptionEditor( + CommunicatorService communicator, + SaveService saveService, + Configuration config) + where TGroup : class, IModGroup + where TOption : class, IModOption { - GroupRenamed, - GroupAdded, - GroupDeleted, - GroupMoved, - GroupTypeChanged, - PriorityChanged, - OptionAdded, - OptionDeleted, - OptionMoved, - OptionFilesChanged, - OptionFilesAdded, - OptionSwapsChanged, - OptionMetaChanged, - DisplayChange, - PrepareChange, - DefaultOptionChanged, -} - -public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) -{ - /// Change the type of a group given by mod and index to type, if possible. - public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) - { - var group = mod.Groups[groupIdx]; - if (group.Type == type) - return; - - mod.Groups[groupIdx] = group.Convert(type); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } - - /// Change the settings stored as default options in a mod. - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, Setting defaultOption) - { - var group = mod.Groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; - - group.DefaultSettings = defaultOption; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } - - /// Rename an option group if possible. - public void RenameModGroup(Mod mod, int groupIdx, string newName) - { - var group = mod.Groups[groupIdx]; - var oldName = group.Name; - if (oldName == newName || !VerifyFileName(mod, group, newName, true)) - return; - - saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - _ = group switch - { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } + protected readonly CommunicatorService Communicator = communicator; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. - public void AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { - if (!VerifyFileName(mod, null, newName, true)) - return; + if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) + return null; - var idx = mod.Groups.Count; - var group = ModGroup.Create(mod, type, newName); + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); - saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); + return group; } /// Add a new mod, empty option group of the given type and name if it does not exist already. - public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + public (TGroup, int, bool) FindOrAddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { var idx = mod.Groups.IndexOf(g => g.Name == newName); if (idx >= 0) - return (mod.Groups[idx], idx, false); + { + var existingGroup = mod.Groups[idx] as TGroup + ?? throw new Exception($"Mod group with name {newName} exists, but is of the wrong type."); + return (existingGroup, idx, false); + } - AddModGroup(mod, type, newName, saveType); - if (mod.Groups[^1].Name != newName) + idx = mod.Groups.Count; + if (AddModGroup(mod, newName, saveType) is not { } group) throw new Exception($"Could not create new mod group with name {newName}."); - return (mod.Groups[^1], mod.Groups.Count - 1, true); - } - - /// Delete a given option group. Fires an event to prepare before actually deleting. - public void DeleteModGroup(Mod mod, int groupIdx) - { - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod.Groups.RemoveAt(groupIdx); - saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - /// Move the index of a given option group. - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) - return; - - saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); - } - - /// Change the description of the given option group. - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) - { - var group = mod.Groups[groupIdx]; - if (group.Description == newDescription) - return; - - group.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - /// Change the description of the given option. - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var option = mod.Groups[groupIdx].Options[optionIdx]; - if (option.Description == newDescription) - return; - - option.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - /// Change the internal priority of the given option group. - public void ChangeGroupPriority(Mod mod, int groupIdx, ModPriority newPriority) - { - var group = mod.Groups[groupIdx]; - if (group.Priority == newPriority) - return; - - group.Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - /// Change the internal priority of the given option. - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, ModPriority newPriority) - { - switch (mod.Groups[groupIdx]) - { - case MultiModGroup multi: - if (multi.OptionData[optionIdx].Priority == newPriority) - return; - - multi.OptionData[optionIdx].Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - /// Rename the given option. - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - var option = mod.Groups[groupIdx].Options[optionIdx]; - if (option.Name == newName) - return; - - option.Name = newName; - - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + return (group, idx, true); } /// Add a new empty option of the given name for the given group. - public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public TOption? AddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var idx = group.AddOption(mod, newName); - if (idx < 0) - return -1; + if (group.AddOption(newName) is not TOption option) + return null; - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return idx; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, option, null, -1); + return option; } /// Add a new empty option of the given name for the given group if it does not exist already. - public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (TOption, int, bool) FindOrAddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var idx = group.Options.IndexOf(o => o.Name == newName); + var idx = group.Options.IndexOf(o => o.Name == newName); if (idx >= 0) - return (group.Options[idx], idx, false); + { + var existingOption = group.Options[idx] as TOption + ?? throw new Exception($"Mod option with name {newName} exists, but is of the wrong type."); // Should never happen. + return (existingOption, idx, false); + } - idx = group.AddOption(mod, newName); - if (idx < 0) + if (AddOption(group, newName, saveType) is not { } option) throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return (group.Options[idx], idx, true); + return (option, idx, true); } /// Add an existing option to a given group. - public void AddOption(Mod mod, int groupIdx, IModOption option) + public TOption? AddOption(TGroup group, IModOption option) { - var group = mod.Groups[groupIdx]; - int idx; - switch (group) - { - case MultiModGroup { OptionData.Count: >= IModGroup.MaxMultiOptions }: - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); - return; - case SingleModGroup s: - { - idx = s.OptionData.Count; - var newOption = new SingleSubMod(s.Mod, s) - { - Name = option.Name, - Description = option.Description, - }; - if (option is IModDataContainer data) - SubMod.Clone(data, newOption); - s.OptionData.Add(newOption); - break; - } - case MultiModGroup m: - { - idx = m.OptionData.Count; - var newOption = new MultiSubMod(m.Mod, m) - { - Name = option.Name, - Description = option.Description, - Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, - }; - if (option is IModDataContainer data) - SubMod.Clone(data, newOption); - m.OptionData.Add(newOption); - break; - } - default: return; - } + if (CloneOption(group, option) is not { } clonedOption) + return null; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, clonedOption, null, -1); + return clonedOption; } /// Delete the given option from the given group. - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + public void DeleteOption(TOption option) { - var group = mod.Groups[groupIdx]; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.OptionData.RemoveAt(optionIdx); - break; - } - - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + var mod = option.Mod; + var group = option.Group; + var optionIdx = option.GetIndex(); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); + RemoveOption((TGroup)group, optionIdx); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, group, null, null, optionIdx); } /// Move an option inside the given option group. - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + public void MoveOption(TOption option, int optionIdxTo) { - var group = mod.Groups[groupIdx]; - if (!group.MoveOption(optionIdxFrom, optionIdxTo)) + var idx = option.GetIndex(); + var group = (TGroup)option.Group; + if (!MoveOption(group, idx, optionIdxTo)) return; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet manipulations, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) - return; - - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); - subMod.Manipulations.SetTo(manipulations); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1); - } - - /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary replacements, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.Files.SetEquals(replacements)) - return; - - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); - subMod.Files.SetTo(replacements); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, dataContainerIdx, -1); - } - - /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. - public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary additions) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - var oldCount = subMod.Files.Count; - subMod.Files.AddFrom(additions); - if (oldCount != subMod.Files.Count) - { - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1); - } - } - - /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary swaps, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.FileSwaps.SetEquals(swaps)) - return; - - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); - subMod.FileSwaps.SetTo(swaps); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1); - } - - - /// Verify that a new option group name is unique in this mod. - public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) - { - var path = newName.RemoveInvalidPathSymbols(); - if (path.Length != 0 - && !mod.Groups.Any(o => !ReferenceEquals(o, group) - && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - return true; - - if (message) - Penumbra.Messager.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - NotificationType.Warning, false); - - return false; - } - - /// Get the correct option for the given group and option index. - private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx) - { - if (groupIdx == -1 && dataContainerIdx == 0) - return mod.Default; - - return mod.Groups[groupIdx].DataContainers[dataContainerIdx]; - } + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TOption? CloneOption(TGroup group, IModOption option); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension diff --git a/Penumbra/Mods/Manager/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/MultiModGroupEditor.cs new file mode 100644 index 00000000..e6b2bac1 --- /dev/null +++ b/Penumbra/Mods/Manager/MultiModGroupEditor.cs @@ -0,0 +1,84 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + public void ChangeToSingle(MultiModGroup group) + { + var idx = group.GetIndex(); + var singleGroup = group.ConvertToSingle(); + group.Mod.Groups[idx] = singleGroup; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1); + } + + /// Change the internal priority of the given option. + public void ChangeOptionPriority(MultiSubMod option, ModPriority newPriority) + { + if (option.Priority == newPriority) + return; + + option.Priority = newPriority; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, option.Mod, option.Group, option, null, -1); + } + + protected override MultiModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override MultiSubMod? CloneOption(MultiModGroup group, IModOption option) + { + if (group.OptionData.Count >= IModGroup.MaxMultiOptions) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " + + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); + return null; + } + + var newOption = new MultiSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + + if (option is IModDataContainer data) + { + SubMod.Clone(data, newOption); + if (option is MultiSubMod m) + newOption.Priority = m.Priority; + else + newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); + } + + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(MultiModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + } + + protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/SingleModGroupEditor.cs new file mode 100644 index 00000000..4999ff60 --- /dev/null +++ b/Penumbra/Mods/Manager/SingleModGroupEditor.cs @@ -0,0 +1,57 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + public void ChangeToMulti(SingleModGroup group) + { + var idx = group.GetIndex(); + var multiGroup = group.ConvertToMulti(); + group.Mod.Groups[idx] = multiGroup; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1); + } + + protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override SingleSubMod CloneOption(SingleModGroup group, IModOption option) + { + var newOption = new SingleSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + if (option is IModDataContainer data) + SubMod.Clone(data, newOption); + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(SingleModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveSingle(optionIndex); + } + + protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 40f943c8..47261c6d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -90,7 +90,7 @@ public partial class ModCreator( var changes = false; foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) { - var group = LoadModGroup(mod, file, mod.Groups.Count); + var group = LoadModGroup(mod, file); if (group != null && mod.Groups.All(g => g.Name != group.Name)) { changes = changes @@ -244,12 +244,12 @@ public partial class ModCreator( { case GroupType.Multi: { - var group = MultiModGroup.CreateForSaving(name); + var group = MultiModGroup.WithoutMod(name); group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + group.OptionData.AddRange(subMods.Select(s => s.Clone(group))); + _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: @@ -258,8 +258,8 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group))); + _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } } @@ -272,7 +272,7 @@ public partial class ModCreator( .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority); + var mod = MultiSubMod.WithoutGroup(option.Name, option.Description, priority); foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); @@ -295,7 +295,7 @@ public partial class ModCreator( } IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1, Config.ReplaceNonAsciiOnImport)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } /// Return the name of a new valid directory based on the base directory and the given name. @@ -422,7 +422,7 @@ public partial class ModCreator( /// Load an option group for a specific mod by its file and index. - private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) + private static IModGroup? LoadModGroup(Mod mod, FileInfo file) { if (!File.Exists(file.FullName)) return null; @@ -442,7 +442,7 @@ public partial class ModCreator( } return null; - } + } internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) { diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index db9e0521..39ee1860 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -4,6 +4,7 @@ using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Settings; @@ -45,63 +46,64 @@ public class ModSettings } // Automatically react to changes in a mods available options. - public bool HandleChanges(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + public bool HandleChanges(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, int fromIdx) { switch (type) { case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: // Add new empty setting for new mod. - Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings); + Settings.Insert(group!.GetIndex(), group.DefaultSettings); return true; case ModOptionChangeType.GroupDeleted: // Remove setting for deleted mod. - Settings.RemoveAt(groupIdx); + Settings.RemoveAt(fromIdx); return true; case ModOptionChangeType.GroupTypeChanged: { // Fix settings for a changed group type. // Single -> Multi: set single as enabled, rest as disabled // Multi -> Single: set the first enabled option or 0. - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var idx = group!.GetIndex(); + var config = Settings[idx]; + Settings[idx] = group.Type switch { GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; - return config != Settings[groupIdx]; + return config != Settings[idx]; } case ModOptionChangeType.OptionDeleted: { // Single -> select the previous option if any. // Multi -> excise the corresponding bit. - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var groupIdx = group!.GetIndex(); + var config = Settings[groupIdx]; + Settings[groupIdx] = group!.Type switch { - GroupType.Single => config.AsIndex >= optionIdx - ? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero - : config, - GroupType.Multi => config.RemoveBit(optionIdx), - _ => config, + GroupType.Single => config.RemoveSingle(fromIdx), + GroupType.Multi => config.RemoveBit(fromIdx), + GroupType.Imc => config.RemoveBit(fromIdx), + _ => config, }; return config != Settings[groupIdx]; } case ModOptionChangeType.GroupMoved: // Move the group the same way. - return Settings.Move(groupIdx, movedToIdx); + return Settings.Move(fromIdx, group!.GetIndex()); case ModOptionChangeType.OptionMoved: { // Single -> select the moved option if it was currently selected // Multi -> move the corresponding bit - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var groupIdx = group!.GetIndex(); + var toIdx = option!.GetIndex(); + var config = Settings[groupIdx]; + Settings[groupIdx] = group!.Type switch { - GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config, - GroupType.Multi => config.MoveBit(optionIdx, movedToIdx), + GroupType.Single => config.MoveSingle(fromIdx, toIdx), + GroupType.Multi => config.MoveBit(fromIdx, toIdx), + GroupType.Imc => config.MoveBit(fromIdx, toIdx), _ => config, }; return config != Settings[groupIdx]; diff --git a/Penumbra/Mods/Settings/Setting.cs b/Penumbra/Mods/Settings/Setting.cs index 231529b8..059cbf51 100644 --- a/Penumbra/Mods/Settings/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -41,6 +41,34 @@ public readonly record struct Setting(ulong Value) public Setting TurnMulti(int count) => new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0)); + public Setting RemoveSingle(int singleIdx) + { + var settingIndex = AsIndex; + if (settingIndex >= singleIdx) + return settingIndex > 1 ? Single(settingIndex - 1) : Zero; + + return this; + } + + public Setting MoveSingle(int singleIdxFrom, int singleIdxTo) + { + var currentIndex = AsIndex; + if (currentIndex == singleIdxFrom) + return Single(singleIdxTo); + + if (singleIdxFrom < singleIdxTo) + { + if (currentIndex > singleIdxFrom && currentIndex <= singleIdxTo) + return Single(currentIndex - 1); + } + else if (currentIndex < singleIdxFrom && currentIndex >= singleIdxTo) + { + return Single(currentIndex + 1); + } + + return this; + } + public ModPriority AsPriority => new((int)(Value & 0xFFFFFFFF)); diff --git a/Penumbra/Mods/SubMods/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs index 83d632a0..ecfcf91a 100644 --- a/Penumbra/Mods/SubMods/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,10 +1,15 @@ +using Penumbra.Mods.Groups; + namespace Penumbra.Mods.SubMods; public interface IModOption { + public Mod Mod { get; } + public IModGroup Group { get; } + public string Name { get; set; } public string FullName { get; } public string Description { get; set; } - public (int GroupIndex, int OptionIndex) GetOptionIndices(); + public int GetIndex(); } diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs new file mode 100644 index 00000000..167c8a6c --- /dev/null +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -0,0 +1,32 @@ +using Penumbra.Mods.Groups; + +namespace Penumbra.Mods.SubMods; + +public class ImcSubMod(ImcModGroup group) : IModOption +{ + public readonly ImcModGroup Group = group; + + public Mod Mod + => Group.Mod; + + public byte AttributeIndex; + + public ushort Attribute + => (ushort)(1 << AttributeIndex); + + Mod IModOption.Mod + => Mod; + + IModGroup IModOption.Group + => Group; + + public string Name { get; set; } = "Part"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + public int GetIndex() + => SubMod.GetIndex(this); +} diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 3bcaffab..c01dcce9 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -4,21 +4,21 @@ using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; -public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(mod, group) +public class MultiSubMod(MultiModGroup group) : OptionSubMod(group) { public ModPriority Priority { get; set; } = ModPriority.Default; - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) - : this(mod, group) + public MultiSubMod(MultiModGroup group, JToken json) + : this(group) { SubMod.LoadOptionData(json, this); - SubMod.LoadDataContainer(json, this, mod.ModPath); + SubMod.LoadDataContainer(json, this, group.Mod.ModPath); Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; } - public MultiSubMod Clone(Mod mod, MultiModGroup group) + public MultiSubMod Clone(MultiModGroup group) { - var ret = new MultiSubMod(mod, group) + var ret = new MultiSubMod(group) { Name = Name, Description = Description, @@ -29,9 +29,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod new(null!, null!) + public static MultiSubMod WithoutGroup(string name, string description, ModPriority priority) + => new(null!) { Name = name, Description = description, diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index fbf03243..02d86af2 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -1,25 +1,26 @@ -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Groups; -using Penumbra.String.Classes; - +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + namespace Penumbra.Mods.SubMods; -public interface IModDataOption : IModDataContainer, IModOption; - -public abstract class OptionSubMod(Mod mod, T group) : IModDataOption - where T : IModGroup +public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContainer { - internal readonly Mod Mod = mod; - internal readonly IModGroup Group = group; + protected readonly IModGroup Group = group; - public string Name { get; set; } = "Option"; + public Mod Mod + => Group.Mod; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; public string FullName - => $"{Group!.Name}: {Name}"; + => $"{Group.Name}: {Name}"; - public string Description { get; set; } = string.Empty; + Mod IModOption.Mod + => Mod; IMod IModDataContainer.Mod => Mod; @@ -27,6 +28,9 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption IModGroup IModDataContainer.Group => Group; + IModGroup IModOption.Group + => Group; + public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; @@ -43,8 +47,8 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); + public int GetIndex() + => SubMod.GetIndex(this); private int GetDataIndex() { @@ -54,4 +58,11 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption return dataIndex; } -} +} + +public abstract class OptionSubMod(T group) : OptionSubMod(group) + where T : IModGroup +{ + public new T Group + => (T)base.Group; +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 98c56151..675f37bc 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -4,18 +4,18 @@ using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; -public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod(mod, singleGroup) +public class SingleSubMod(SingleModGroup singleGroup) : OptionSubMod(singleGroup) { - public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json) - : this(mod, singleGroup) + public SingleSubMod(SingleModGroup singleGroup, JToken json) + : this(singleGroup) { SubMod.LoadOptionData(json, this); - SubMod.LoadDataContainer(json, this, mod.ModPath); + SubMod.LoadDataContainer(json, this, singleGroup.Mod.ModPath); } - public SingleSubMod Clone(Mod mod, SingleModGroup group) + public SingleSubMod Clone(SingleModGroup group) { - var ret = new SingleSubMod(mod, group) + var ret = new SingleSubMod(group) { Name = Name, Description = Description, @@ -25,9 +25,9 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod group switch - { - SingleModGroup single => new SingleSubMod(group.Mod, single) - { - Name = name, - Description = description, - }, - MultiModGroup multi => new MultiSubMod(group.Mod, multi) - { - Name = name, - Description = description, - }, - _ => throw new ArgumentOutOfRangeException(nameof(group)), - }; + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static int GetIndex(IModOption option) + { + var dataIndex = option.Group.Options.IndexOf(option); + if (dataIndex < 0) + throw new Exception($"Group {option.Group.Name} from option {option.Name} does not contain this option."); + + return dataIndex; + } /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void AddContainerTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) { @@ -37,6 +32,7 @@ public static class SubMod } /// Replace all data of with the data of . + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void Clone(IModDataContainer from, IModDataContainer to) { to.Files = new Dictionary(from.Files); @@ -45,6 +41,7 @@ public static class SubMod } /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath) { data.Files.Clear(); @@ -75,6 +72,7 @@ public static class SubMod } /// Load the relevant data for a selectable option from a JToken of that option. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void LoadOptionData(JToken json, IModOption option) { option.Name = json[nameof(option.Name)]?.ToObject() ?? string.Empty; @@ -82,6 +80,7 @@ public static class SubMod } /// Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { j.WritePropertyName(nameof(data.Files)); @@ -111,6 +110,7 @@ public static class SubMod } /// Write the data for a selectable mod option on a JsonWriter. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(option.Name)); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 0ec1fd44..2d595ec1 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,6 +23,12 @@ PROFILING; + + + + + + PreserveNewest @@ -93,10 +99,6 @@ - - - - diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 8d3cb641..eff3295d 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -34,8 +34,11 @@ public sealed class SaveService(Logger log, FrameworkManager framework, Filename } } - for (var i = 0; i < mod.Groups.Count - 1; ++i) - ImmediateSave(new ModSaveGroup(mod, i, onlyAscii)); - ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1, onlyAscii)); + if (mod.Groups.Count > 0) + { + foreach (var group in mod.Groups.SkipLast(1)) + ImmediateSave(new ModSaveGroup(group, onlyAscii)); + ImmediateSaveSync(new ModSaveGroup(mod.Groups[^1], onlyAscii)); + } } } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index e758aa35..5fa1a848 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -121,7 +121,6 @@ public static class StaticServiceManager private static ServiceManager AddMods(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 77bdb161..cd55beb0 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -17,6 +17,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -264,9 +265,10 @@ public class ItemSwapTab : IDisposable, ITab return; _modManager.AddMod(newDir); - if (!_swapData.WriteMod(_modManager, _modManager[^1], + var mod = _modManager[^1]; + if (!_swapData.WriteMod(_modManager, mod, mod.Default, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) - _modManager.DeleteMod(_modManager[^1]); + _modManager.DeleteMod(mod); } private void CreateOption() @@ -276,7 +278,7 @@ public class ItemSwapTab : IDisposable, ITab var groupCreated = false; var dirCreated = false; - var optionCreated = -1; + IModOption? createdOption = null; DirectoryInfo? optionFolderName = null; try { @@ -290,22 +292,22 @@ public class ItemSwapTab : IDisposable, ITab { if (_selectedGroup == null) { - _modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName); - _selectedGroup = _mod.Groups.Last(); + if (_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName) is not { } group) + throw new Exception($"Failure creating option group."); + + _selectedGroup = group; groupCreated = true; } - var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); - if (optionIdx < 0) + if (_modManager.OptionEditor.AddOption(_selectedGroup, _newOptionName) is not { } option) throw new Exception($"Failure creating mod option."); - optionCreated = optionIdx; + createdOption = option; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if (!_swapData.WriteMod(_modManager, _mod, - _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, - optionFolderName, - _mod.Groups.IndexOf(_selectedGroup), optionIdx)) + // #TODO ModOption <> DataContainer + if (!_swapData.WriteMod(_modManager, _mod, (IModDataContainer)option, + _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName)) throw new Exception("Failure writing files for mod swap."); } } @@ -314,12 +316,12 @@ public class ItemSwapTab : IDisposable, ITab Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { - if (optionCreated >= 0 && _selectedGroup != null) - _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); + if (createdOption != null) + _modManager.OptionEditor.DeleteOption(createdOption); if (groupCreated) { - _modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); + _modManager.OptionEditor.DeleteModGroup(_selectedGroup!); _selectedGroup = null; } @@ -717,7 +719,8 @@ public class ItemSwapTab : IDisposable, ITab _dirty = true; } - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int fromIdx) { if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod) return; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 92a9dd66..743310ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -27,7 +27,7 @@ public partial class ModEditWindow private const string GenderTooltip = "Gender"; private const string ObjectTypeTooltip = "Object Type"; private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIDTooltip = "Primary ID"; + private const string PrimaryIdTooltipShort = "Primary ID"; private const string VariantIdTooltip = "Variant ID"; private const string EstTypeTooltip = "EST Type"; private const string RacialTribeTooltip = "Racial Tribe"; @@ -45,7 +45,7 @@ public partial class ModEditWindow var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); + _editor.MetaEditor.Apply(_editor.Option!); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -477,7 +477,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIDTooltip); + ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index dbb88fb7..6b48a048 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); + _editor.SwapEditor.Apply(_editor.Option!); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -627,7 +627,7 @@ public partial class ModEditWindow : Window, IDisposable public void Dispose() { _communicator.ModPathChanged.Unsubscribe(OnModPathChange); - _editor?.Dispose(); + _editor.Dispose(); _materialTab.Dispose(); _modelTab.Dispose(); _shaderPackageTab.Dispose(); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index afbef45d..fcd76a51 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -15,6 +15,7 @@ using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab; @@ -248,13 +249,13 @@ public class ModPanelEditTab( ImGui.SameLine(); - var nameValid = ModOptionEditor.VerifyFileName(mod, null, _newGroupName, false); + var nameValid = ModGroupEditor.VerifyFileName(mod, null, _newGroupName, false); tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, !nameValid, true)) return; - modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName); + modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName); Reset(); } } @@ -364,9 +365,9 @@ public class ModPanelEditTab( break; case >= 0: if (_newDescriptionOptionIdx < 0) - modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription); else - modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, + modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx], _newDescription); break; @@ -396,18 +397,18 @@ public class ModPanelEditTab( .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.OptionEditor.RenameModGroup(_mod, groupIdx, newGroupName); + _modManager.OptionEditor.RenameModGroup(group, newGroupName); ImGuiUtil.HoverTooltip("Group Name"); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group)); ImGui.SameLine(); if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority); + _modManager.OptionEditor.ChangeGroupPriority(group, priority); ImGuiUtil.HoverTooltip("Group Priority"); @@ -417,7 +418,7 @@ public class ModPanelEditTab( var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1)); ImGui.SameLine(); tt = groupIdx == _mod.Groups.Count - 1 @@ -425,7 +426,7 @@ public class ModPanelEditTab( : $"Move this group down to group {groupIdx + 2}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1)); ImGui.SameLine(); @@ -452,17 +453,17 @@ public class ModPanelEditTab( { private const string DragDropLabel = "##DragOption"; - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static int _dragDropGroupIdx = -1; - private static int _dragDropOptionIdx = -1; + private static int _newOptionNameIdx = -1; + private static string _newOptionName = string.Empty; + private static IModGroup? _dragDropGroup; + private static IModOption? _dragDropOption; public static void Reset() { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroup = null; + _dragDropOption = null; } public static void Draw(ModPanelEditTab panel, int groupIdx) @@ -482,7 +483,7 @@ public class ModPanelEditTab( switch (panel._mod.Groups[groupIdx]) { - case SingleModGroup single: + case SingleModGroup single: for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) EditOption(panel, single, groupIdx, optionIdx); break; @@ -491,6 +492,7 @@ public class ModPanelEditTab( EditOption(panel, multi, groupIdx, optionIdx); break; } + DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); } @@ -502,8 +504,8 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Selectable($"Option #{optionIdx + 1}"); - Source(group, groupIdx, optionIdx); - Target(panel, group, groupIdx, optionIdx); + Source(option); + Target(panel, group, optionIdx); ImGui.TableNextColumn(); @@ -511,7 +513,7 @@ public class ModPanelEditTab( if (group.Type == GroupType.Single) { if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, Setting.Single(optionIdx)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } @@ -519,15 +521,14 @@ public class ModPanelEditTab( { var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, - group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } ImGui.TableNextColumn(); if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + panel._modManager.OptionEditor.RenameOption(option, newOptionName); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", @@ -537,15 +538,15 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option)); ImGui.TableNextColumn(); - if (group is not MultiModGroup multi) + if (option is not MultiSubMod multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority, 50 * UiHelpers.Scale)) - panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority); ImGuiUtil.HoverTooltip("Option priority."); } @@ -564,7 +565,7 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Selectable($"Option #{count + 1}"); - Target(panel, group, groupIdx, count); + Target(panel, group, count); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); @@ -585,14 +586,14 @@ public class ModPanelEditTab( tt, !(canAddGroup && validName), true)) return; - panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName); + panel._modManager.OptionEditor.AddOption(group, _newOptionName); _newOptionName = string.Empty; } // Handle drag and drop to move options inside a group or into another group. - private static void Source(IModGroup group, int groupIdx, int optionIdx) + private static void Source(IModOption option) { - if (group is not ITexToolsGroup) + if (option.Group is not ITexToolsGroup) return; using var source = ImRaii.DragDropSource(); @@ -601,14 +602,14 @@ public class ModPanelEditTab( if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) { - _dragDropGroupIdx = groupIdx; - _dragDropOptionIdx = optionIdx; + _dragDropGroup = option.Group; + _dragDropOption = option; } - ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}..."); + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); } - private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx) { if (group is not ITexToolsGroup) return; @@ -617,39 +618,53 @@ public class ModPanelEditTab( if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) return; - if (_dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0) + if (_dragDropGroup != null && _dragDropOption != null) { - if (_dragDropGroupIdx == groupIdx) + if (_dragDropGroup == group) { - var sourceOption = _dragDropOptionIdx; + var sourceOption = _dragDropOption; panel._delayedActions.Enqueue( - () => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); } else { // Move from one group to another by deleting, then adding, then moving the option. - var sourceGroupIdx = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group.DataContainers.Count; - var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx]; + var sourceOption = _dragDropOption; panel._delayedActions.Enqueue(() => { - panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); - panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + panel._modManager.OptionEditor.DeleteOption(sourceOption); + if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + panel._modManager.OptionEditor.MoveOption(newOption, optionIdx); }); } } - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; + _dragDropGroup = null; + _dragDropOption = null; } } /// Draw a combo to select single or multi group and switch between them. private void DrawGroupCombo(IModGroup group, int groupIdx) { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); + using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); + if (!combo) + return; + + if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m) + _modManager.OptionEditor.MultiEditor.ChangeToSingle(m); + + var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; + using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); + if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s) + _modManager.OptionEditor.SingleEditor.ChangeToMulti(s); + + style.Pop(); + if (!canSwitchToMulti) + ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); + return; + static string GroupTypeName(GroupType type) => type switch { @@ -657,23 +672,6 @@ public class ModPanelEditTab( GroupType.Multi => "Multi Group", _ => "Unknown", }; - - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); - using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); - if (!combo) - return; - - if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) - _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); - - var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); - if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) - _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); - - style.Pop(); - if (!canSwitchToMulti) - ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); } /// Handles input text and integers in separate fields without buffers for every single one. From cff617245356a3721d3e5849585e89856cbebcc5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 27 Apr 2024 00:07:10 +0200 Subject: [PATCH 070/865] Move editors into folder. --- OtterGui | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 1 + .../Cache/CollectionCacheManager.cs | 1 + .../Collections/Manager/CollectionStorage.cs | 1 + Penumbra/Communication/ModOptionChanged.cs | 1 + Penumbra/Mods/Editor/ModMerger.cs | 1 + Penumbra/Mods/Manager/ModCacheManager.cs | 1 + Penumbra/Mods/Manager/ModManager.cs | 1 + .../{ => OptionEditor}/ImcModGroupEditor.cs | 4 +- .../{ => OptionEditor}/ModGroupEditor.cs | 37 +++++++------- .../{ => OptionEditor}/ModOptionEditor.cs | 50 +++++++++---------- .../{ => OptionEditor}/MultiModGroupEditor.cs | 8 +-- .../SingleModGroupEditor.cs | 8 +-- Penumbra/Mods/Settings/ModSettings.cs | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/packages.lock.json | 14 ++++-- 18 files changed, 74 insertions(+), 62 deletions(-) rename Penumbra/Mods/Manager/{ => OptionEditor}/ImcModGroupEditor.cs (91%) rename Penumbra/Mods/Manager/{ => OptionEditor}/ModGroupEditor.cs (88%) rename Penumbra/Mods/Manager/{ => OptionEditor}/ModOptionEditor.cs (73%) rename Penumbra/Mods/Manager/{ => OptionEditor}/MultiModGroupEditor.cs (92%) rename Penumbra/Mods/Manager/{ => OptionEditor}/SingleModGroupEditor.cs (90%) diff --git a/OtterGui b/OtterGui index 3460a817..20c4a6c5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3460a817fc5e01a6b60eb834c3c59031938388fc +Subproject commit 20c4a6c53103d9fa8dec63babc628c9d01f094c0 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index bfd134bb..56b80e63 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -10,6 +10,7 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 9c104cef..fb9ee9a3 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -9,6 +9,7 @@ using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index bfae2dc0..de5d0a14 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -6,6 +6,7 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index a20592ec..67f2c0c3 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -3,6 +3,7 @@ using Penumbra.Api.Api; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using static Penumbra.Communication.ModOptionChanged; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 3a6f4a81..d6e21076 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -5,6 +5,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 0669696f..59c88cf0 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -3,6 +3,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 7a266a31..adaca85e 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,5 +1,6 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Services; namespace Penumbra.Mods.Manager; diff --git a/Penumbra/Mods/Manager/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs similarity index 91% rename from Penumbra/Mods/Manager/ImcModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 4e2b2194..1194f961 100644 --- a/Penumbra/Mods/Manager/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -6,7 +6,7 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService @@ -14,7 +14,7 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; diff --git a/Penumbra/Mods/Manager/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs similarity index 88% rename from Penumbra/Mods/Manager/ModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 9f41fa6f..b2b48ac0 100644 --- a/Penumbra/Mods/Manager/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -1,10 +1,8 @@ -using System.Text.RegularExpressions; using Dalamud.Interface.Internal.Notifications; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -12,9 +10,8 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.Util; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public enum ModOptionChangeType { @@ -91,7 +88,7 @@ public class ModGroupEditor( /// Move the index of a given option group. public void MoveModGroup(IModGroup group, int groupIdxTo) { - var mod = group.Mod; + var mod = group.Mod; var idxFrom = group.GetIndex(); if (!mod.Groups.Move(idxFrom, groupIdxTo)) return; @@ -230,45 +227,45 @@ public class ModGroupEditor( => group switch { SingleModGroup s => SingleEditor.AddOption(s, option), - MultiModGroup m => MultiEditor.AddOption(m, option), - ImcModGroup i => ImcEditor.AddOption(i, option), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, }; public IModOption? AddOption(IModGroup group, string newName) => group switch { SingleModGroup s => SingleEditor.AddOption(s, newName), - MultiModGroup m => MultiEditor.AddOption(m, newName), - ImcModGroup i => ImcEditor.AddOption(i, newName), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, }; public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), - GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), - _ => null, + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + _ => null, }; public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), - _ => (null, -1, false), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), }; public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) => group switch { SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), - MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), - ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), - _ => (null, -1, false), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), }; public void MoveOption(IModOption option, int toIdx) diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs similarity index 73% rename from Penumbra/Mods/Manager/ModOptionEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs index 7370a933..c067102e 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs @@ -5,7 +5,7 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public abstract class ModOptionEditor( CommunicatorService communicator, @@ -15,8 +15,8 @@ public abstract class ModOptionEditor( where TOption : class, IModOption { protected readonly CommunicatorService Communicator = communicator; - protected readonly SaveService SaveService = saveService; - protected readonly Configuration Config = config; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) @@ -25,7 +25,7 @@ public abstract class ModOptionEditor( return null; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, maxPriority); + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -92,8 +92,8 @@ public abstract class ModOptionEditor( /// Delete the given option from the given group. public void DeleteOption(TOption option) { - var mod = option.Mod; - var group = option.Group; + var mod = option.Mod; + var group = option.Group; var optionIdx = option.GetIndex(); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); RemoveOption((TGroup)group, optionIdx); @@ -104,7 +104,7 @@ public abstract class ModOptionEditor( /// Move an option inside the given option group. public void MoveOption(TOption option, int optionIdxTo) { - var idx = option.GetIndex(); + var idx = option.GetIndex(); var group = (TGroup)option.Group; if (!MoveOption(group, idx, optionIdxTo)) return; @@ -113,10 +113,10 @@ public abstract class ModOptionEditor( Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); protected abstract TOption? CloneOption(TGroup group, IModOption option); - protected abstract void RemoveOption(TGroup group, int optionIndex); - protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension @@ -132,22 +132,22 @@ public static class ModOptionChangeTypeExtension { (requiresSaving, requiresReloading, wasPrepared) = type switch { - ModOptionChangeType.GroupRenamed => (true, false, false), - ModOptionChangeType.GroupAdded => (true, false, false), - ModOptionChangeType.GroupDeleted => (true, true, false), - ModOptionChangeType.GroupMoved => (true, false, false), - ModOptionChangeType.GroupTypeChanged => (true, true, true), - ModOptionChangeType.PriorityChanged => (true, true, true), - ModOptionChangeType.OptionAdded => (true, true, true), - ModOptionChangeType.OptionDeleted => (true, true, false), - ModOptionChangeType.OptionMoved => (true, false, false), - ModOptionChangeType.OptionFilesChanged => (false, true, false), - ModOptionChangeType.OptionFilesAdded => (false, true, true), - ModOptionChangeType.OptionSwapsChanged => (false, true, false), - ModOptionChangeType.OptionMetaChanged => (false, true, false), - ModOptionChangeType.DisplayChange => (false, false, false), + ModOptionChangeType.GroupRenamed => (true, false, false), + ModOptionChangeType.GroupAdded => (true, false, false), + ModOptionChangeType.GroupDeleted => (true, true, false), + ModOptionChangeType.GroupMoved => (true, false, false), + ModOptionChangeType.GroupTypeChanged => (true, true, true), + ModOptionChangeType.PriorityChanged => (true, true, true), + ModOptionChangeType.OptionAdded => (true, true, true), + ModOptionChangeType.OptionDeleted => (true, true, false), + ModOptionChangeType.OptionMoved => (true, false, false), + ModOptionChangeType.OptionFilesChanged => (false, true, false), + ModOptionChangeType.OptionFilesAdded => (false, true, true), + ModOptionChangeType.OptionSwapsChanged => (false, true, false), + ModOptionChangeType.OptionMetaChanged => (false, true, false), + ModOptionChangeType.DisplayChange => (false, false, false), ModOptionChangeType.DefaultOptionChanged => (true, false, false), - _ => (false, false, false), + _ => (false, false, false), }; } } diff --git a/Penumbra/Mods/Manager/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs similarity index 92% rename from Penumbra/Mods/Manager/MultiModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index e6b2bac1..c48d2d40 100644 --- a/Penumbra/Mods/Manager/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -6,14 +6,14 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService { public void ChangeToSingle(MultiModGroup group) { - var idx = group.GetIndex(); + var idx = group.GetIndex(); var singleGroup = group.ConvertToSingle(); group.Mod.Groups[idx] = singleGroup; SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); @@ -34,7 +34,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe protected override MultiModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; @@ -50,7 +50,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe var newOption = new MultiSubMod(group) { - Name = option.Name, + Name = option.Name, Description = option.Description, }; diff --git a/Penumbra/Mods/Manager/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs similarity index 90% rename from Penumbra/Mods/Manager/SingleModGroupEditor.cs rename to Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 4999ff60..556416c6 100644 --- a/Penumbra/Mods/Manager/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -6,14 +6,14 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods.Manager.OptionEditor; public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService { public void ChangeToMulti(SingleModGroup group) { - var idx = group.GetIndex(); + var idx = group.GetIndex(); var multiGroup = group.ConvertToMulti(); group.Mod.Groups[idx] = multiGroup; SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); @@ -23,7 +23,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; @@ -31,7 +31,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS { var newOption = new SingleSubMod(group) { - Name = option.Name, + Name = option.Name, Description = option.Description, }; if (option is IModDataContainer data) diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 39ee1860..7fe48365 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -3,7 +3,7 @@ using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Settings; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2d595ec1..2e53bd22 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -75,7 +75,7 @@ - + diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index cd55beb0..4db0df47 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -16,6 +16,7 @@ using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index fcd76a51..862852fa 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -16,6 +16,7 @@ using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; +using Penumbra.Mods.Manager.OptionEditor; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 68e1b880..b431e595 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -34,9 +34,14 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.3, )", - "resolved": "3.1.3", - "contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg==" + "requested": "[3.1.4, )", + "resolved": "3.1.4", + "contentHash": "lFIdxgGDA5iYkUMRFOze7BGLcdpoLFbR+a20kc1W7NepvzU7ejtxtWOg9RvgG7kb9tBoJ3ONYOK6kLil/dgF1w==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2023.3.0", + "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", @@ -75,6 +80,7 @@ "ottergui": { "type": "Project", "dependencies": { + "JetBrains.Annotations": "[2023.3.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" } }, @@ -88,7 +94,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[1.0.15, )", + "Penumbra.Api": "[5.0.0, )", "Penumbra.String": "[1.0.4, )" } }, From 9084b43e3ebef4e00471aa8839ed29aecd0fc25a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Apr 2024 12:01:54 +0200 Subject: [PATCH 071/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9208c9c2..1b0b5469 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9208c9c242244beeb3c1fb826582d72da09831af +Subproject commit 1b0b5469f792999e5b412d4f0c3ed77d9994d7b7 From 2e76148fba396e8c0e3fc0dc8ad632e0726e059d Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:14:40 +1000 Subject: [PATCH 072/865] Ensure materials end in .mtrl --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b8c0176a..b3460667 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -290,10 +290,13 @@ public partial class ModEditWindow /// While materials can be relative (`/mt_...`) or absolute (`bg/...`), /// they invariably must contain at least one directory seperator. /// Missing this can lead to a crash. + /// + /// They must also be at least one character (though this is enforced + /// by containing a `/`), and end with `.mtrl`. /// public bool ValidateMaterial(string material) { - return material.Contains('/'); + return material.Contains('/') && material.EndsWith(".mtrl"); } /// Remove the material given by the index. From c96adcf557597a712582b09eee62978ad479f2b7 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:15:00 +1000 Subject: [PATCH 073/865] Prevent saving invalid files --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c891d33a..66f38bab 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -204,8 +204,9 @@ public class FileEditor( private void SaveButton() { + var canSave = _changed && _currentFile != null && _currentFile.Valid; if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, - $"Save the selected {fileType} file with all changes applied. This is not revertible.", !_changed)) + $"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave)) { compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); if (owner.Mod != null) From 078688454a5e67a05497f92b0a834e9c5daae594 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:17:54 +1000 Subject: [PATCH 074/865] Show an invalid material count --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 03b5169a..e075b592 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -293,7 +293,21 @@ public partial class ModEditWindow private bool DrawModelMaterialDetails(MdlTab tab, bool disabled) { - if (!ImGui.CollapsingHeader("Materials")) + var invalidMaterialCount = tab.Mdl.Materials.Count(material => !tab.ValidateMaterial(material)); + + var oldPos = ImGui.GetCursorPosY(); + var header = ImGui.CollapsingHeader("Materials"); + var newPos = ImGui.GetCursorPos(); + if (invalidMaterialCount > 0) + { + var text = $"{invalidMaterialCount} invalid material{(invalidMaterialCount > 1 ? "s" : "")}"; + var size = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(0xFF0000FF, text); + ImGui.SetCursorPos(newPos); + } + + if (!header) return false; using var table = ImRaii.Table(string.Empty, disabled ? 2 : 4, ImGuiTableFlags.SizingFixedFit); @@ -382,7 +396,8 @@ public partial class ModEditWindow { ImGuiComponents.HelpMarker( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" - + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\").", + + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" + + "and must end in \".mtrl\".", FontAwesomeIcon.TimesCircle); } From 9063d131bae492f5186b2a96d01e8135f7e2364f Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 21:41:35 +1000 Subject: [PATCH 075/865] Use validation logic for new material field --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index e075b592..0be95c99 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -337,7 +337,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); ImGui.InputTextWithHint("##newMaterial", "Add new material...", ref _modelNewMaterial, Utf8GamePath.MaxGamePathLength, inputFlags); - var validName = _modelNewMaterial.Length > 0 && _modelNewMaterial[0] == '/'; + var validName = tab.ValidateMaterial(_modelNewMaterial); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) { @@ -346,6 +346,8 @@ public partial class ModEditWindow _modelNewMaterial = string.Empty; } ImGui.TableNextColumn(); + if (!validName && _modelNewMaterial.Length > 0) + DrawInvalidMaterialMarker(); return ret; } @@ -392,18 +394,22 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Add markers to invalid materials. if (!tab.ValidateMaterial(temp)) - using (var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true)) - { - ImGuiComponents.HelpMarker( - "Materials must be either relative (e.g. \"/filename.mtrl\")\n" - + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" - + "and must end in \".mtrl\".", - FontAwesomeIcon.TimesCircle); - } + DrawInvalidMaterialMarker(); return ret; } + private void DrawInvalidMaterialMarker() + { + using var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true); + + ImGuiComponents.HelpMarker( + "Materials must be either relative (e.g. \"/filename.mtrl\")\n" + + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" + + "and must end in \".mtrl\".", + FontAwesomeIcon.TimesCircle); + } + private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) { using var lodNode = ImRaii.TreeNode($"Level of Detail #{lodIndex + 1}", ImGuiTreeNodeFlags.DefaultOpen); From 46a111d15287112a09ee3acf0b7cba0a30454ef2 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 3 May 2024 22:42:28 +1000 Subject: [PATCH 076/865] Fix marker --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0be95c99..a7d39c6e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -401,13 +401,13 @@ public partial class ModEditWindow private void DrawInvalidMaterialMarker() { - using var colorHandle = ImRaii.PushColor(ImGuiCol.TextDisabled, 0xFF0000FF, true); + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); - ImGuiComponents.HelpMarker( + ImGuiUtil.HoverTooltip( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" + "or absolute (e.g. \"bg/full/path/to/filename.mtrl\"),\n" - + "and must end in \".mtrl\".", - FontAwesomeIcon.TimesCircle); + + "and must end in \".mtrl\"."); } private bool DrawModelLodDetails(MdlTab tab, int lodIndex, bool disabled) From 36fc251d5b4265c9fb6d0af8c9cfc88233c71697 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 3 May 2024 18:52:52 +0200 Subject: [PATCH 077/865] Fix a bunch of issues. --- Penumbra.GameData | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 19 +- Penumbra/Mods/Groups/ImcModGroup.cs | 4 +- Penumbra/Mods/Groups/ModSaveGroup.cs | 7 +- Penumbra/Mods/Groups/MultiModGroup.cs | 4 +- Penumbra/Mods/Groups/SingleModGroup.cs | 13 +- .../OptionEditor/MultiModGroupEditor.cs | 4 +- .../OptionEditor/SingleModGroupEditor.cs | 4 +- Penumbra/Mods/SubMods/SubMod.cs | 1 - Penumbra/Services/BackupService.cs | 1 + Penumbra/Services/StaticServiceManager.cs | 1 - Penumbra/UI/ModsTab/ModGroupDrawer.cs | 233 +++++++++++++++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 214 ++++++++++++++ Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 272 ++---------------- 14 files changed, 513 insertions(+), 266 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ModGroupDrawer.cs create mode 100644 Penumbra/UI/ModsTab/ModGroupEditDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 1b0b5469..595ac572 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1b0b5469f792999e5b412d4f0c3ed77d9994d7b7 +Subproject commit 595ac5722c9c400bea36110503ed2ae7b02d1489 diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index a268ba0f..d7138434 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -12,16 +12,23 @@ public interface ITexToolsGroup public IReadOnlyList OptionData { get; } } +public enum GroupDrawBehaviour +{ + SingleSelection, + MultiSelection, +} + public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; set; } - public string Description { get; set; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; set; } + public string Description { get; set; } + public GroupType Type { get; } + public GroupDrawBehaviour Behaviour { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); public IModOption? AddOption(string name, string description = ""); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e233f82e..e58d855a 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -21,6 +21,9 @@ public class ImcModGroup(Mod mod) : IModGroup public GroupType Type => GroupType.Imc; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + public ModPriority Priority { get; set; } = ModPriority.Default; public Setting DefaultSettings { get; set; } = Setting.Zero; @@ -150,7 +153,6 @@ public class ImcModGroup(Mod mod) : IModGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } public (int Redirections, int Swaps, int Manips) GetCounts() diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index efdcde09..ed3c8857 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -56,10 +56,10 @@ public readonly struct ModSaveGroup : ISavable { _basePath = (container.Mod as Mod)?.ModPath ?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen. - _defaultMod = null; + _defaultMod = container as DefaultSubMod; _onlyAscii = onlyAscii; - _group = container.Group!; - _groupIdx = _group.GetIndex(); + _group = container.Group; + _groupIdx = _group?.GetIndex() ?? -1; } public string ToFilename(FilenameService fileNames) @@ -80,7 +80,6 @@ public readonly struct ModSaveGroup : ISavable public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) { - jWriter.WriteStartObject(); jWriter.WritePropertyName(nameof(group.Name)); jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index a0034be0..7495c4b4 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -17,6 +17,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; @@ -127,7 +130,6 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } public (int Redirections, int Swaps, int Manips) GetCounts() diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 0776c2af..459cec4a 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -15,7 +15,10 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; } = mod; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.SingleSelection; + + public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public ModPriority Priority { get; set; } @@ -89,7 +92,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); + { + if (!IsOption) + return; + + OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); + } public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); @@ -111,7 +119,6 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } /// Create a group without a mod only for saving it in the creator. diff --git a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index c48d2d40..74362325 100644 --- a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -16,8 +16,8 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe var idx = group.GetIndex(); var singleGroup = group.ConvertToSingle(); group.Mod.Groups[idx] = singleGroup; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1); + SaveService.QueueSave(new ModSaveGroup(singleGroup, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, singleGroup.Mod, singleGroup, null, null, -1); } /// Change the internal priority of the given option. diff --git a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 556416c6..15a899a0 100644 --- a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -16,8 +16,8 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS var idx = group.GetIndex(); var multiGroup = group.ConvertToMulti(); group.Mod.Groups[idx] = multiGroup; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1); + SaveService.QueueSave(new ModSaveGroup(multiGroup, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, multiGroup.Mod, multiGroup, null, null, -1); } protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a50e397f..b984b570 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -106,7 +106,6 @@ public static class SubMod j.WriteEndObject(); j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); - j.WriteEndObject(); } /// Write the data for a selectable mod option on a JsonWriter. diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index a542dab5..bd5b3bcc 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -29,6 +29,7 @@ public class BackupService : IAsyncService list.Add(new FileInfo(fileNames.ConfigFile)); list.Add(new FileInfo(fileNames.FilesystemFile)); list.Add(new FileInfo(fileNames.ActiveCollectionsFile)); + list.Add(new FileInfo(fileNames.PredefinedTagFile)); return list; } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 5fa1a848..19ae31a2 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -162,7 +162,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/ModsTab/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/ModGroupDrawer.cs new file mode 100644 index 00000000..e9b0b396 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModGroupDrawer.cs @@ -0,0 +1,233 @@ +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab; + +public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService +{ + private readonly List<(IModGroup, int)> _blockGroupCache = []; + + public void Draw(Mod mod, ModSettings settings) + { + if (mod.Groups.Count <= 0) + return; + + _blockGroupCache.Clear(); + var useDummy = true; + foreach (var (group, idx) in mod.Groups.WithIndex()) + { + if (!group.IsOption) + continue; + + switch (group.Behaviour) + { + case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax: + case GroupDrawBehaviour.MultiSelection: + _blockGroupCache.Add((group, idx)); + break; + + case GroupDrawBehaviour.SingleSelection: + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + DrawSingleGroupCombo(group, idx, settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]); + break; + } + } + + useDummy = true; + foreach (var (group, idx) in _blockGroupCache) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + var option = settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]; + if (group.Behaviour is GroupDrawBehaviour.MultiSelection) + DrawMultiGroup(group, idx, option); + else + DrawSingleGroupRadio(group, idx, option); + } + } + + /// + /// Draw a single group selector as a combo box. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); + var options = group.Options; + using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) + { + if (combo) + for (var idx2 = 0; idx2 < options.Count; ++idx2) + { + id.Push(idx2); + var option = options[idx2]; + if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + SetModSetting(group, groupIdx, Setting.Single(idx2)); + + if (option.Description.Length > 0) + ImGuiUtil.SelectableHelpMarker(option.Description); + + id.Pop(); + } + } + + ImGui.SameLine(); + if (group.Description.Length > 0) + ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + else + ImGui.TextUnformatted(group.Name); + } + + /// + /// Draw a single group selector as a set of radio buttons. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + return; + + void DrawOptions() + { + for (var idx = 0; idx < group.Options.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = options[idx]; + if (ImGui.RadioButton(option.Name, selectedOption == idx)) + SetModSetting(group, groupIdx, Setting.Single(idx)); + + if (option.Description.Length <= 0) + continue; + + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + /// + /// Draw a multi group selector as a bordered set of checkboxes. + /// If a description is provided, add a help marker in the title. + /// + private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + DrawMultiPopup(group, groupIdx, label); + return; + + void DrawOptions() + { + for (var idx = 0; idx < options.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); + + if (ImGui.Checkbox(option.Name, ref enabled)) + SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + } + + private void DrawMultiPopup(IModGroup group, int groupIdx, string label) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); + using var popup = ImRaii.Popup(label); + if (!popup) + return; + + ImGui.TextUnformatted(group.Name); + ImGui.Separator(); + if (ImGui.Selectable("Enable All")) + SetModSetting(group, groupIdx, Setting.AllBits(group.Options.Count)); + + if (ImGui.Selectable("Disable All")) + SetModSetting(group, groupIdx, Setting.Zero); + } + + private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) + { + if (options.Count <= config.OptionGroupCollapsibleMin) + { + draw(); + } + else + { + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var buttonTextShow = $"Show {options.Count} Options"; + var buttonTextHide = $"Hide {options.Count} Options"; + var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + + 2 * ImGui.GetStyle().FramePadding.X; + minWidth = Math.Max(buttonWidth, minWidth); + if (shown) + { + var pos = ImGui.GetCursorPos(); + ImGui.Dummy(UiHelpers.IconButtonSize); + using (var _ = ImRaii.Group()) + { + draw(); + } + + + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var endPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(pos); + if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + + ImGui.SetCursorPos(endPos); + } + else + { + var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + + ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetFrameHeight() + + ImGui.GetStyle().FramePadding.X; + var width = Math.Max(optionWidth, minWidth); + if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + } + } + } + + private ModCollection Current + => collectionManager.Active.Current; + + private void SetModSetting(IModGroup group, int groupIdx, Setting setting) + => collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs new file mode 100644 index 00000000..6b62d5b8 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -0,0 +1,214 @@ +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Services; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public sealed class ModGroupEditDrawer(ModManager modManager, Configuration config, FilenameService filenames) : IUiService +{ + private Vector2 _buttonSize; + private float _priorityWidth; + private float _groupNameWidth; + private float _spacing; + + private string? _currentGroupName; + private ModPriority? _currentGroupPriority; + private IModGroup? _currentGroupEdited; + private bool _isGroupNameValid; + private IModGroup? _deleteGroup; + private IModGroup? _moveGroup; + private int _moveTo; + + private string? _currentOptionName; + private ModPriority? _currentOptionPriority; + private IModOption? _currentOptionEdited; + private IModOption? _deleteOption; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SameLine() + => ImGui.SameLine(0, _spacing); + + public void Draw(Mod mod) + { + _buttonSize = new Vector2(ImGui.GetFrameHeight()); + _priorityWidth = 50 * ImGuiHelpers.GlobalScale; + _groupNameWidth = 350f * ImGuiHelpers.GlobalScale; + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + + + FinishGroupCleanup(); + } + + private void FinishGroupCleanup() + { + if (_deleteGroup != null) + { + modManager.OptionEditor.DeleteModGroup(_deleteGroup); + _deleteGroup = null; + } + + if (_deleteOption != null) + { + modManager.OptionEditor.DeleteOption(_deleteOption); + _deleteOption = null; + } + + if (_moveGroup != null) + { + modManager.OptionEditor.MoveModGroup(_moveGroup, _moveTo); + _moveGroup = null; + } + } + + private void DrawGroup(IModGroup group, int idx) + { + using var id = ImRaii.PushId(idx); + using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); + DrawGroupNameRow(group); + switch (group) + { + case SingleModGroup s: + DrawSingleGroup(s, idx); + break; + case MultiModGroup m: + DrawMultiGroup(m, idx); + break; + case ImcModGroup i: + DrawImcGroup(i, idx); + break; + } + } + + private void DrawGroupNameRow(IModGroup group) + { + DrawGroupName(group); + SameLine(); + DrawGroupDelete(group); + SameLine(); + DrawGroupPriority(group); + } + + private void DrawGroupName(IModGroup group) + { + var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; + ImGui.SetNextItemWidth(_groupNameWidth); + using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); + if (ImGui.InputText("##GroupName", ref text, 256)) + { + _currentGroupEdited = group; + _currentGroupName = text; + _isGroupNameValid = ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupName != null && _isGroupNameValid) + modManager.OptionEditor.RenameModGroup(group, _currentGroupName); + _currentGroupName = null; + _currentGroupEdited = null; + _isGroupNameValid = true; + } + + var tt = _isGroupNameValid + ? "Group Name" + : "Current name can not be used for this group."; + ImGuiUtil.HoverTooltip(tt); + } + + private void DrawGroupDelete(IModGroup group) + { + var enabled = config.DeleteModModifier.IsActive(); + var tt = enabled + ? "Delete this option group." + : $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."; + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, tt, !enabled, true)) + _deleteGroup = group; + } + + private void DrawGroupPriority(IModGroup group) + { + var priority = _currentGroupEdited == group + ? (_currentGroupPriority ?? group.Priority).Value + : group.Priority.Value; + ImGui.SetNextItemWidth(_priorityWidth); + if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) + { + _currentGroupEdited = group; + _currentGroupPriority = new ModPriority(priority); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupPriority.HasValue) + modManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); + _currentGroupEdited = null; + _currentGroupPriority = null; + } + + ImGuiUtil.HoverTooltip("Group Priority"); + } + + private void DrawGroupMoveButtons(IModGroup group, int idx) + { + var isFirst = idx == 0; + var tt = isFirst ? "Can not move this group further upwards." : $"Move this group up to group {idx}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, isFirst, true)) + { + _moveGroup = group; + _moveTo = idx - 1; + } + + SameLine(); + var isLast = idx == group.Mod.Groups.Count - 1; + tt = isLast + ? "Can not move this group further downwards." + : $"Move this group down to group {idx + 2}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, isLast, true)) + { + _moveGroup = group; + _moveTo = idx + 1; + } + } + + private void DrawGroupOpenFile(IModGroup group, int idx) + { + var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); + var fileExists = File.Exists(fileName); + var tt = fileExists + ? $"Open the {group.Name} json file in the text editor of your choice." + : $"The {group.Name} json file does not exist."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); + } + } + + + private void DrawSingleGroup(SingleModGroup group, int idx) + { } + + private void DrawMultiGroup(MultiModGroup group, int idx) + { } + + private void DrawImcGroup(ImcModGroup group, int idx) + { } +} diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1326a763..fc5311d9 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,51 +1,37 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; -using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.UI.Classes; -using Dalamud.Interface.Components; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Mods.SubMods; -using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; -public class ModPanelSettingsTab : ITab +public class ModPanelSettingsTab( + CollectionManager collectionManager, + ModManager modManager, + ModFileSystemSelector selector, + TutorialService tutorial, + CommunicatorService communicator, + ModGroupDrawer modGroupDrawer) + : ITab, IUiService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly CollectionManager _collectionManager; - private readonly ModFileSystemSelector _selector; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private bool _inherited; private ModSettings _settings = null!; private ModCollection _collection = null!; - private bool _empty; private int? _currentPriority; - public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, - TutorialService tutorial, CommunicatorService communicator, Configuration config) - { - _collectionManager = collectionManager; - _communicator = communicator; - _modManager = modManager; - _selector = selector; - _tutorial = tutorial; - _config = config; - } - public ReadOnlySpan Label => "Settings"u8; public void DrawHeader() - => _tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); + => tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); public void Reset() => _currentPriority = null; @@ -56,53 +42,24 @@ public class ModPanelSettingsTab : ITab if (!child) return; - _settings = _selector.SelectedSettings; - _collection = _selector.SelectedSettingCollection; - _inherited = _collection != _collectionManager.Active.Current; - _empty = _settings == ModSettings.Empty; - + _settings = selector.SelectedSettings; + _collection = selector.SelectedSettingCollection; + _inherited = _collection != collectionManager.Active.Current; DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - _communicator.PreSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); + communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier); DrawEnabledInput(); - _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); + tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); DrawPriorityInput(); - _tutorial.OpenTutorial(BasicTutorialSteps.Priority); + tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); - _communicator.PostEnabledDraw.Invoke(_selector.Selected!.Identifier); - - if (_selector.Selected!.Groups.Count > 0) - { - var useDummy = true; - foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() - .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax)) - { - ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); - useDummy = false; - DrawSingleGroupCombo(group, idx); - } - - useDummy = true; - foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex().Where(g => g.Value.IsOption)) - { - ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); - useDummy = false; - switch (group.Type) - { - case GroupType.Multi: - DrawMultiGroup(group, idx); - break; - case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax: - DrawSingleGroupRadio(group, idx); - break; - } - } - } + communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier); + modGroupDrawer.Draw(selector.Selected!, _settings); UiHelpers.DefaultLineSpace(); - _communicator.PostSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); + communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier); } /// Draw a big red bar if the current setting is inherited. @@ -113,8 +70,8 @@ public class ModPanelSettingsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, false); + if (ImUtf8.Button($"These settings are inherited from {_collection.Name}.", width)) + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -127,8 +84,8 @@ public class ModPanelSettingsTab : ITab if (!ImGui.Checkbox("Enabled", ref enabled)) return; - _modManager.SetKnown(_selector.Selected!); - _collectionManager.Editor.SetModState(_collectionManager.Active.Current, _selector.Selected!, enabled); + modManager.SetKnown(selector.Selected!); + collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled); } /// @@ -146,7 +103,8 @@ public class ModPanelSettingsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != _settings.Priority.Value) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, new ModPriority(_currentPriority.Value)); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -162,189 +120,15 @@ public class ModPanelSettingsTab : ITab private void DrawRemoveSettings() { const string text = "Inherit Settings"; - if (_inherited || _empty) + if (_inherited || _settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, true); + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); } - - /// - /// Draw a single group selector as a combo box. - /// If a description is provided, add a help marker besides it. - /// - private void DrawSingleGroupCombo(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - var options = group.Options; - using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) - { - if (combo) - for (var idx2 = 0; idx2 < options.Count; ++idx2) - { - id.Push(idx2); - var option = options[idx2]; - if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.Single(idx2)); - - if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); - - id.Pop(); - } - } - - ImGui.SameLine(); - if (group.Description.Length > 0) - ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); - else - ImGui.TextUnformatted(group.Name); - } - - // Draw a single group selector as a set of radio buttons. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupRadio(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; - DrawCollapseHandling(options, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - return; - - void DrawOptions() - { - for (var idx = 0; idx < group.Options.Count; ++idx) - { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.Single(idx)); - - if (option.Description.Length <= 0) - continue; - - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } - } - } - - - private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) - { - if (options.Count <= _config.OptionGroupCollapsibleMin) - { - draw(); - } - else - { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); - var buttonTextShow = $"Show {options.Count} Options"; - var buttonTextHide = $"Hide {options.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) - + 2 * ImGui.GetStyle().FramePadding.X; - minWidth = Math.Max(buttonWidth, minWidth); - if (shown) - { - var pos = ImGui.GetCursorPos(); - ImGui.Dummy(UiHelpers.IconButtonSize); - using (var _ = ImRaii.Group()) - { - draw(); - } - - - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); - var endPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos(pos); - if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) - ImGui.GetStateStorage().SetBool(collapseId, !shown); - - ImGui.SetCursorPos(endPos); - } - else - { - var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) - + ImGui.GetStyle().ItemInnerSpacing.X - + ImGui.GetFrameHeight() - + ImGui.GetStyle().FramePadding.X; - var width = Math.Max(optionWidth, minWidth); - if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) - ImGui.GetStateStorage().SetBool(collapseId, !shown); - } - } - } - - /// - /// Draw a multi group selector as a bordered set of checkboxes. - /// If a description is provided, add a help marker in the title. - /// - private void DrawMultiGroup(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; - DrawCollapseHandling(options, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); - - DrawMultiPopup(group, groupIdx, label); - return; - - void DrawOptions() - { - for (var idx = 0; idx < options.Count; ++idx) - { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var setting = flags.HasFlag(idx); - - if (ImGui.Checkbox(option.Name, ref setting)) - { - flags = flags.SetBit(idx, setting); - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); - } - - if (option.Description.Length > 0) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } - } - } - } - - private void DrawMultiPopup(IModGroup group, int groupIdx, string label) - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); - using var popup = ImRaii.Popup(label); - if (!popup) - return; - - ImGui.TextUnformatted(group.Name); - ImGui.Separator(); - if (ImGui.Selectable("Enable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.AllBits(group.Options.Count)); - - if (ImGui.Selectable("Disable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); - } } From 7553b5da8ac44fe4d058934033a5aa18ff32ffa1 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sat, 4 May 2024 04:01:28 +1000 Subject: [PATCH 078/865] Fix float imprecision on blend weights --- .../Import/Models/Import/VertexAttribute.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b7f5dcf1..a4651776 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -138,7 +138,27 @@ public class VertexAttribute return new VertexAttribute( element, - index => BuildNByte4(values[index]) + index => { + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off + // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak + // the converted values to have the expected sum, preferencing values with minimal differences. + var originalValues = values[index]; + var byteValues = BuildNByte4(originalValues); + + var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + while (adjustment != 0) + { + var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); + var closestIndex = Enumerable.Range(0, 4) + .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .MinBy(x => x.delta) + .index; + byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); + adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + } + + return byteValues; + } ); } From 2a5df2dfb05469a15cf1386e70139a93eaad1d7d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 May 2024 11:19:07 +0200 Subject: [PATCH 079/865] Create permanent backup before migrating collections. --- Penumbra/Configuration.cs | 2 +- Penumbra/Services/BackupService.cs | 14 ++++++++++++-- Penumbra/Services/ConfigMigrationService.cs | 16 +++++++++++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 98668e8a..a065bc26 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -136,7 +136,7 @@ public class Configuration : IPluginConfiguration, ISavable /// Contains some default values or boundaries for config values. public static class Constants { - public const int CurrentVersion = 8; + public const int CurrentVersion = 9; public const float MaxAbsoluteSize = 600; public const int DefaultAbsoluteSize = 250; public const float MinAbsoluteSize = 50; diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index bd5b3bcc..88b99de1 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -7,6 +7,10 @@ namespace Penumbra.Services; public class BackupService : IAsyncService { + private readonly Logger _logger; + private readonly DirectoryInfo _configDirectory; + private readonly IReadOnlyList _fileNames; + /// public Task Awaiter { get; } @@ -17,10 +21,16 @@ public class BackupService : IAsyncService /// Start a backup process on the collected files. public BackupService(Logger logger, FilenameService fileNames) { - var files = PenumbraFiles(fileNames); - Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files)); + _logger = logger; + _fileNames = PenumbraFiles(fileNames); + _configDirectory = new DirectoryInfo(fileNames.ConfigDirectory); + Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), _fileNames)); } + /// Create a permanent backup with a given name for migrations. + public void CreateMigrationBackup(string name) + => Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name); + /// Collect all relevant files for penumbra configuration. private static IReadOnlyList PenumbraFiles(FilenameService fileNames) { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index fafaa0e5..1f6ac170 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -22,7 +22,7 @@ namespace Penumbra.Services; /// Contains everything to migrate from older versions of the config to the current, /// including deprecated fields. /// -public class ConfigMigrationService(SaveService saveService) : IService +public class ConfigMigrationService(SaveService saveService, BackupService backupService) : IService { private Configuration _config = null!; private JObject _data = null!; @@ -73,9 +73,23 @@ public class ConfigMigrationService(SaveService saveService) : IService Version5To6(); Version6To7(); Version7To8(); + Version8To9(); AddColors(config, true); } + // Migrate to ephemeral config. + private void Version8To9() + { + if (_config.Version != 8) + return; + + backupService.CreateMigrationBackup("pre_collection_identifiers"); + _config.Version = 9; + _config.Ephemeral.Version = 9; + _config.Save(); + _config.Ephemeral.Save(); + } + // Migrate to ephemeral config. private void Version7To8() { From d8dad91e899a4055e5fe19dbc1255333a07f9033 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 May 2024 11:24:01 +0200 Subject: [PATCH 080/865] Oop, not yet. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index fc5311d9..107a2d04 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -2,7 +2,6 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; -using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.UI.Classes; @@ -70,7 +69,7 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImUtf8.Button($"These settings are inherited from {_collection.Name}.", width)) + if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" From 1f2f66b1141f83613c3799faf80439b1142bd571 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 4 May 2024 11:34:02 +0200 Subject: [PATCH 081/865] Meh. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 20c4a6c5..bc2afed8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 20c4a6c53103d9fa8dec63babc628c9d01f094c0 +Subproject commit bc2afed8a873d1f9517eefe7a7296bc5b83e693b From bbbf65eb4c0c9d8dd77ac8f8101ad2ac86433c1c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 May 2024 15:49:49 +0200 Subject: [PATCH 082/865] Fix bug preventing deduplication. --- Penumbra/Mods/Editor/DuplicateManager.cs | 3 +++ Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 84a832a2..2e6dc1d6 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -84,7 +84,10 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (useModManager) modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); else + { + subMod.Files = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport)); + } } } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index ed3c8857..7efc76a6 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -40,7 +40,7 @@ public readonly struct ModSaveGroup : ISavable _basePath = basePath; _defaultMod = container as DefaultSubMod; _onlyAscii = onlyAscii; - if (_defaultMod == null) + if (_defaultMod != null) { _groupIdx = -1; _group = null; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index de2b6a34..bff0092a 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -89,7 +89,7 @@ public class CollectionSelectHeader var collection = _resolver.PlayerCollection(); return CheckCollection(collection) switch { - CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), + CollectionState.Empty => (collection, "None", "The loaded player character is configured to use no mods.", true), CollectionState.Selected => (collection, collection.Name, "The collection configured to apply to the loaded player character is already selected as the current collection.", true), CollectionState.Available => (collection, collection.Name, From 32dbf419e254ca432055f6e4b08b684b5a03447b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 9 May 2024 19:58:35 +0200 Subject: [PATCH 083/865] Fix single group default options not applying. --- Penumbra/Mods/Editor/DuplicateManager.cs | 2 ++ Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 2e6dc1d6..47aa18dc 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -82,7 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; if (useModManager) + { modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); + } else { subMod.Files = dict; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 459cec4a..bc463c1e 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -93,7 +93,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { - if (!IsOption) + if (OptionData.Count == 0) return; OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); From d47d31b665ffa4cd14774f04888a6c50c7a37a72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 May 2024 18:10:59 +0200 Subject: [PATCH 084/865] achieve feature parity... I think. --- OtterGui | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/UI/ModsTab/DescriptionEditPopup.cs | 114 ++++++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 424 ++++++++++++++++---- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 420 +------------------ 5 files changed, 473 insertions(+), 489 deletions(-) create mode 100644 Penumbra/UI/ModsTab/DescriptionEditPopup.cs diff --git a/OtterGui b/OtterGui index bc2afed8..866389b3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bc2afed8a873d1f9517eefe7a7296bc5b83e693b +Subproject commit 866389b3988d9c4926a786f6c78ac9d5265591ac diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index d7138434..2ec60f7e 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -20,7 +20,7 @@ public enum GroupDrawBehaviour public interface IModGroup { - public const int MaxMultiOptions = 63; + public const int MaxMultiOptions = 32; public Mod Mod { get; } public string Name { get; set; } diff --git a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs new file mode 100644 index 00000000..c284afc3 --- /dev/null +++ b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs @@ -0,0 +1,114 @@ +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab; + +public class DescriptionEditPopup(ModManager modManager) : IUiService +{ + private static ReadOnlySpan PopupId + => "PenumbraEditDescription"u8; + + private bool _hasBeenEdited; + private string _description = string.Empty; + + private object? _current; + private bool _opened; + + public void Open(Mod mod) + { + _current = mod; + _opened = true; + _hasBeenEdited = false; + _description = mod.Description; + } + + public void Open(IModGroup group) + { + _current = group; + _opened = true; + _hasBeenEdited = false; + _description = group.Description; + } + + public void Open(IModOption option) + { + _current = option; + _opened = true; + _hasBeenEdited = false; + _description = option.Description; + } + + public void Draw() + { + if (_current == null) + return; + + if (_opened) + { + _opened = false; + ImUtf8.OpenPopup(PopupId); + } + + var inputSize = ImGuiHelpers.ScaledVector2(800); + using var popup = ImUtf8.Popup(PopupId); + if (!popup) + return; + + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + + ImUtf8.InputMultiLineOnDeactivated("##editDescription"u8, ref _description, inputSize); + _hasBeenEdited |= ImGui.IsItemEdited(); + UiHelpers.DefaultLineSpace(); + + var buttonSize = new Vector2(ImUtf8.GlobalScale * 100, 0); + + var width = 2 * buttonSize.X + + 4 * ImUtf8.FramePadding.X + + ImUtf8.ItemSpacing.X; + + ImGui.SetCursorPosX((inputSize.X - width) / 2); + DrawSaveButton(buttonSize); + ImGui.SameLine(); + DrawCancelButton(buttonSize); + } + + private void DrawSaveButton(Vector2 buttonSize) + { + if (!ImUtf8.ButtonEx("Save"u8, _hasBeenEdited ? [] : "No changes made yet."u8, buttonSize, !_hasBeenEdited)) + return; + + switch (_current) + { + case Mod mod: + modManager.DataEditor.ChangeModDescription(mod, _description); + break; + case IModGroup group: + modManager.OptionEditor.ChangeGroupDescription(group, _description); + break; + case IModOption option: + modManager.OptionEditor.ChangeOptionDescription(option, _description); + break; + } + + _description = string.Empty; + _hasBeenEdited = false; + ImGui.CloseCurrentPopup(); + } + + private void DrawCancelButton(Vector2 buttonSize) + { + if (!ImUtf8.Button("Cancel"u8, buttonSize) && !ImGui.IsKeyPressed(ImGuiKey.Escape)) + return; + + _description = string.Empty; + _hasBeenEdited = false; + ImGui.CloseCurrentPopup(); + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index 6b62d5b8..5652fa98 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,11 +1,12 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.EndObjects; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -17,87 +18,79 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public sealed class ModGroupEditDrawer(ModManager modManager, Configuration config, FilenameService filenames) : IUiService +public sealed class ModGroupEditDrawer( + ModManager modManager, + Configuration config, + FilenameService filenames, + DescriptionEditPopup descriptionPopup) : IUiService { + private static ReadOnlySpan DragDropLabel + => "##DragOption"u8; + private Vector2 _buttonSize; + private Vector2 _availableWidth; private float _priorityWidth; private float _groupNameWidth; + private float _optionNameWidth; private float _spacing; + private Vector2 _optionIdxSelectable; + private bool _deleteEnabled; private string? _currentGroupName; private ModPriority? _currentGroupPriority; private IModGroup? _currentGroupEdited; - private bool _isGroupNameValid; - private IModGroup? _deleteGroup; - private IModGroup? _moveGroup; - private int _moveTo; + private bool _isGroupNameValid = true; - private string? _currentOptionName; - private ModPriority? _currentOptionPriority; - private IModOption? _currentOptionEdited; - private IModOption? _deleteOption; + private string? _newOptionName; + private IModGroup? _newOptionGroup; + private readonly Queue _actionQueue = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SameLine() - => ImGui.SameLine(0, _spacing); + private IModGroup? _dragDropGroup; + private IModOption? _dragDropOption; public void Draw(Mod mod) { - _buttonSize = new Vector2(ImGui.GetFrameHeight()); - _priorityWidth = 50 * ImGuiHelpers.GlobalScale; - _groupNameWidth = 350f * ImGuiHelpers.GlobalScale; - _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + PrepareStyle(); + using var id = ImUtf8.PushId("##GroupEdit"u8); + foreach (var (group, groupIdx) in mod.Groups.WithIndex()) + DrawGroup(group, groupIdx); - FinishGroupCleanup(); - } - - private void FinishGroupCleanup() - { - if (_deleteGroup != null) - { - modManager.OptionEditor.DeleteModGroup(_deleteGroup); - _deleteGroup = null; - } - - if (_deleteOption != null) - { - modManager.OptionEditor.DeleteOption(_deleteOption); - _deleteOption = null; - } - - if (_moveGroup != null) - { - modManager.OptionEditor.MoveModGroup(_moveGroup, _moveTo); - _moveGroup = null; - } + while (_actionQueue.TryDequeue(out var action)) + action.Invoke(); } private void DrawGroup(IModGroup group, int idx) { - using var id = ImRaii.PushId(idx); + using var id = ImUtf8.PushId(idx); using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); - DrawGroupNameRow(group); + DrawGroupNameRow(group, idx); switch (group) { case SingleModGroup s: - DrawSingleGroup(s, idx); + DrawSingleGroup(s); break; case MultiModGroup m: - DrawMultiGroup(m, idx); + DrawMultiGroup(m); break; case ImcModGroup i: - DrawImcGroup(i, idx); + DrawImcGroup(i); break; } } - private void DrawGroupNameRow(IModGroup group) + private void DrawGroupNameRow(IModGroup group, int idx) { DrawGroupName(group); - SameLine(); + ImUtf8.SameLineInner(); + DrawGroupMoveButtons(group, idx); + ImUtf8.SameLineInner(); + DrawGroupOpenFile(group, idx); + ImUtf8.SameLineInner(); + DrawGroupDescription(group); + ImUtf8.SameLineInner(); DrawGroupDelete(group); - SameLine(); + ImUtf8.SameLineInner(); DrawGroupPriority(group); } @@ -106,11 +99,11 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; ImGui.SetNextItemWidth(_groupNameWidth); using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); - if (ImGui.InputText("##GroupName", ref text, 256)) + if (ImUtf8.InputText("##GroupName"u8, ref text)) { _currentGroupEdited = group; _currentGroupName = text; - _isGroupNameValid = ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); } if (ImGui.IsItemDeactivated()) @@ -123,20 +116,21 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf } var tt = _isGroupNameValid - ? "Group Name" - : "Current name can not be used for this group."; - ImGuiUtil.HoverTooltip(tt); + ? "Change the Group name."u8 + : "Current name can not be used for this group."u8; + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); } private void DrawGroupDelete(IModGroup group) { - var enabled = config.DeleteModModifier.IsActive(); - var tt = enabled - ? "Delete this option group." - : $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."; + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteModGroup(group)); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, tt, !enabled, true)) - _deleteGroup = group; + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option group."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); } private void DrawGroupPriority(IModGroup group) @@ -162,36 +156,41 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf ImGuiUtil.HoverTooltip("Group Priority"); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawGroupDescription(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) + descriptionPopup.Open(group); + } + private void DrawGroupMoveButtons(IModGroup group, int idx) { var isFirst = idx == 0; - var tt = isFirst ? "Can not move this group further upwards." : $"Move this group up to group {idx}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, isFirst, true)) - { - _moveGroup = group; - _moveTo = idx - 1; - } + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx - 1)); - SameLine(); + if (isFirst) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); + else + ImUtf8.HoverTooltip($"Move this group up to group {idx}."); + + + ImUtf8.SameLineInner(); var isLast = idx == group.Mod.Groups.Count - 1; - tt = isLast - ? "Can not move this group further downwards." - : $"Move this group down to group {idx + 2}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, isLast, true)) - { - _moveGroup = group; - _moveTo = idx + 1; - } + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx + 1)); + + if (isLast) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); + else + ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); } private void DrawGroupOpenFile(IModGroup group, int idx) { var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); var fileExists = File.Exists(fileName); - var tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) try { Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); @@ -200,15 +199,274 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf { Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); } + + if (fileExists) + ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); } + private void DrawSingleGroup(SingleModGroup group) + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); - private void DrawSingleGroup(SingleModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionDefaultSingleBehaviour(group, option, optionIdx); - private void DrawMultiGroup(MultiModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionName(option); - private void DrawImcGroup(ImcModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(_priorityWidth, 0)); + } + + DrawNewOption(group); + var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; + if (ImUtf8.ButtonEx("Convert to Multi Group", _availableWidth, !convertible)) + _actionQueue.Enqueue(() => modManager.OptionEditor.SingleEditor.ChangeToMulti(group)); + if (!convertible) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Can not convert to multi group since maximum number of options is exceeded."u8); + } + + private void DrawMultiGroup(MultiModGroup group) + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionName(option); + + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + DrawOptionPriority(option); + } + + DrawNewOption(group); + if (ImUtf8.Button("Convert to Single Group"u8, _availableWidth)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MultiEditor.ChangeToSingle(group)); + } + + private void DrawImcGroup(ImcModGroup group) + { + // TODO + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) + { + ImGui.AlignTextToFramePadding(); + ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: _optionIdxSelectable); + Target(group, optionIdx); + Source(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; + if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); + ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); + if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDescription(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) + descriptionPopup.Open(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionPriority(MultiSubMod option) + { + var priority = option.Priority.Value; + ImGui.SetNextItemWidth(_priorityWidth); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + modManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); + ImUtf8.HoverTooltip("Option priority inside the mod."u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionName(IModOption option) + { + var name = option.Name; + ImGui.SetNextItemWidth(_optionNameWidth); + if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) + modManager.OptionEditor.RenameOption(option, name); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDelete(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteOption(option)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + private void DrawNewOption(SingleModGroup group) + { + var count = group.Options.Count; + if (count >= int.MaxValue) + return; + + DrawNewOptionBase(group, count); + + var validName = _newOptionName?.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.SingleEditor.AddOption(group, _newOptionName!); + _newOptionName = null; + } + } + + private void DrawNewOption(MultiModGroup group) + { + var count = group.Options.Count; + if (count >= IModGroup.MaxMultiOptions) + return; + + DrawNewOptionBase(group, count); + + var validName = _newOptionName?.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.MultiEditor.AddOption(group, _newOptionName!); + _newOptionName = null; + } + } + + private void DrawNewOption(ImcModGroup group) + { + // TODO + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawNewOptionBase(IModGroup group, int count) + { + ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); + Target(group, count); + + ImUtf8.SameLineInner(); + ImUtf8.IconDummy(); + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(_optionNameWidth); + var newName = _newOptionGroup == group + ? _newOptionName ?? string.Empty + : string.Empty; + if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) + { + _newOptionName = newName; + _newOptionGroup = group; + } + + ImUtf8.SameLineInner(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Source(IModOption option) + { + if (option.Group is not ITexToolsGroup) + return; + + using var source = ImUtf8.DragDropSource(); + if (!source) + return; + + if (!DragDropSource.SetPayload(DragDropLabel)) + { + _dragDropGroup = option.Group; + _dragDropOption = option; + } + + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + } + + private void Target(IModGroup group, int optionIdx) + { + if (group is not ITexToolsGroup) + return; + + if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + return; + + if (_dragDropGroup != null && _dragDropOption != null) + { + if (_dragDropGroup == group) + { + var sourceOption = _dragDropOption; + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceOption = _dragDropOption; + _actionQueue.Enqueue(() => + { + modManager.OptionEditor.DeleteOption(sourceOption); + if (modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + modManager.OptionEditor.MoveOption(newOption, optionIdx); + }); + } + } + + _dragDropGroup = null; + _dragDropOption = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepareStyle() + { + var totalWidth = 400f * ImUtf8.GlobalScale; + _buttonSize = new Vector2(ImUtf8.FrameHeight); + _priorityWidth = 50 * ImUtf8.GlobalScale; + _availableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + _priorityWidth, 0); + _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + _optionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); + _optionNameWidth = totalWidth - _optionIdxSelectable.X - _buttonSize.X - 2 * _spacing; + _deleteEnabled = config.DeleteModModifier.IsActive(); + } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 862852fa..a5db15b6 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,21 +1,17 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; -using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; -using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.Mods.SubMods; using Penumbra.Mods.Manager.OptionEditor; namespace Penumbra.UI.ModsTab; @@ -30,15 +26,13 @@ public class ModPanelEditTab( FilenameService filenames, ModExportManager modExportManager, Configuration config, - PredefinedTagManager predefinedTagManager) + PredefinedTagManager predefinedTagManager, + ModGroupEditDrawer groupEditDrawer, + DescriptionEditPopup descriptionPopup) : ITab { - private readonly ModManager _modManager = modManager; - private readonly TagButtons _modTags = new(); - private Vector2 _cellPadding = Vector2.Zero; - private Vector2 _itemSpacing = Vector2.Zero; private ModFileSystem.Leaf _leaf = null!; private Mod _mod = null!; @@ -54,9 +48,6 @@ public class ModPanelEditTab( _leaf = selector.SelectedLeaf!; _mod = selector.Selected!; - _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; - _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; - EditButtons(); EditRegularMeta(); UiHelpers.DefaultLineSpace(); @@ -77,21 +68,18 @@ public class ModPanelEditTab( 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); if (tagIdx >= 0) - _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); + modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); if (sharedTagsEnabled) predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false, selector.Selected!); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(filenames, _modManager, _mod, config.ReplaceNonAsciiOnImport); + AddOptionGroup.Draw(filenames, modManager, _mod, config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); - for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) - EditGroup(groupIdx); - - EndActions(); - DescriptionEdit.DrawPopup(_modManager); + groupEditDrawer.Draw(_mod); + descriptionPopup.Draw(); } public void Reset() @@ -99,7 +87,6 @@ public class ModPanelEditTab( AddOptionGroup.Reset(); MoveDirectory.Reset(); Input.Reset(); - OptionTable.Reset(); } /// The general edit row for non-detailed mod edits. @@ -117,10 +104,10 @@ public class ModPanelEditTab( if (ImGuiUtil.DrawDisabledButton("Reload Mod", buttonSize, "Reload the current mod from its files.\n" + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", false)) - _modManager.ReloadMod(_mod); + modManager.ReloadMod(_mod); BackupButtons(buttonSize); - MoveDirectory.Draw(_modManager, _mod, buttonSize); + MoveDirectory.Draw(modManager, _mod, buttonSize); UiHelpers.DefaultLineSpace(); DrawUpdateBibo(buttonSize); @@ -169,7 +156,7 @@ public class ModPanelEditTab( : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) - backup.Restore(_modManager); + backup.Restore(modManager); if (backup.Exists) { ImGui.SameLine(); @@ -186,24 +173,24 @@ public class ModPanelEditTab( private void EditRegularMeta() { if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModName(_mod, newName); + modManager.DataEditor.ChangeModName(_mod, newName); if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); + modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModVersion(_mod, newVersion); + modManager.DataEditor.ChangeModVersion(_mod, newVersion); if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); + modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); if (ImGui.Button("Edit Description", reducedSize)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + descriptionPopup.Open(_mod); ImGui.SameLine(); var fileExists = File.Exists(filenames.ModMetaPath(_mod)); @@ -215,16 +202,6 @@ public class ModPanelEditTab( Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } - /// Do some edits outside of iterations. - private readonly Queue _delayedActions = new(); - - /// Delete a marked group or option outside of iteration. - private void EndActions() - { - while (_delayedActions.TryDequeue(out var action)) - action.Invoke(); - } - /// Text input to add a new option group at the end of the current groups. private static class AddOptionGroup { @@ -309,372 +286,6 @@ public class ModPanelEditTab( } } - /// Open a popup to edit a multi-line mod or option description. - private static class DescriptionEdit - { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static string _oldDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDescriptionOptionIdx = -1; - private static Mod? _mod; - - public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) - { - _newDescriptionIdx = groupIdx; - _newDescriptionOptionIdx = optionIdx; - _newDescription = groupIdx < 0 - ? mod.Description - : optionIdx < 0 - ? mod.Groups[groupIdx].Description - : mod.Groups[groupIdx].Options[optionIdx].Description; - _oldDescription = _newDescription; - - _mod = mod; - ImGui.OpenPopup(PopupName); - } - - public static void DrawPopup(ModManager modManager) - { - if (_mod == null) - return; - - using var popup = ImRaii.Popup(PopupName); - if (!popup) - return; - - if (ImGui.IsWindowAppearing()) - ImGui.SetKeyboardFocusHere(); - - ImGui.InputTextMultiline("##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2(800, 800)); - UiHelpers.DefaultLineSpace(); - - var buttonSize = ImGuiHelpers.ScaledVector2(100, 0); - var width = 2 * buttonSize.X - + 4 * ImGui.GetStyle().FramePadding.X - + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX((800 * UiHelpers.Scale - width) / 2); - - var tooltip = _newDescription != _oldDescription ? string.Empty : "No changes made yet."; - - if (ImGuiUtil.DrawDisabledButton("Save", buttonSize, tooltip, tooltip.Length > 0)) - { - switch (_newDescriptionIdx) - { - case Input.Description: - modManager.DataEditor.ChangeModDescription(_mod, _newDescription); - break; - case >= 0: - if (_newDescriptionOptionIdx < 0) - modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription); - else - modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx], - _newDescription); - - break; - } - - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if (!ImGui.Button("Cancel", buttonSize) - && !ImGui.IsKeyPressed(ImGuiKey.Escape)) - return; - - _newDescriptionIdx = Input.None; - _newDescription = string.Empty; - ImGui.CloseCurrentPopup(); - } - } - - private void EditGroup(int groupIdx) - { - var group = _mod.Groups[groupIdx]; - using var id = ImRaii.PushId(groupIdx); - using var frame = ImRaii.FramedGroup($"Group #{groupIdx + 1}"); - - using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, _cellPadding) - .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); - - if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.OptionEditor.RenameModGroup(group, newGroupName); - - ImGuiUtil.HoverTooltip("Group Name"); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group)); - - ImGui.SameLine(); - - if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.OptionEditor.ChangeGroupPriority(group, priority); - - ImGuiUtil.HoverTooltip("Group Priority"); - - DrawGroupCombo(group, groupIdx); - ImGui.SameLine(); - - var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, - tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1)); - - ImGui.SameLine(); - tt = groupIdx == _mod.Groups.Count - 1 - ? "Can not move this group further downwards." - : $"Move this group down to group {groupIdx + 2}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, - tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1)); - - ImGui.SameLine(); - - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, - "Edit group description.", false, true)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); - - ImGui.SameLine(); - var fileName = filenames.OptionGroupFile(_mod, groupIdx, config.ReplaceNonAsciiOnImport); - var fileExists = File.Exists(fileName); - tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - - UiHelpers.DefaultLineSpace(); - - OptionTable.Draw(this, groupIdx); - } - - /// Draw the table displaying all options and the add new option line. - private static class OptionTable - { - private const string DragDropLabel = "##DragOption"; - - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static IModGroup? _dragDropGroup; - private static IModOption? _dragDropOption; - - public static void Reset() - { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroup = null; - _dragDropOption = null; - } - - public static void Draw(ModPanelEditTab panel, int groupIdx) - { - using var table = ImRaii.Table(string.Empty, 6, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - var maxWidth = ImGui.CalcTextSize("Option #88.").X; - ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, maxWidth); - ImGui.TableSetupColumn("default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, - UiHelpers.InputTextWidth.X - maxWidth - 12 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("description", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); - - switch (panel._mod.Groups[groupIdx]) - { - case SingleModGroup single: - for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) - EditOption(panel, single, groupIdx, optionIdx); - break; - case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) - EditOption(panel, multi, groupIdx, optionIdx); - break; - } - - DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); - } - - /// Draw a line for a single option. - private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) - { - var option = group.Options[optionIdx]; - using var id = ImRaii.PushId(optionIdx); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{optionIdx + 1}"); - Source(option); - Target(panel, group, optionIdx); - - ImGui.TableNextColumn(); - - - if (group.Type == GroupType.Single) - { - if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); - - ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); - } - else - { - var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); - if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); - - ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); - } - - ImGui.TableNextColumn(); - if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.OptionEditor.RenameOption(option, newOptionName); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", - false, true)) - panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option)); - - ImGui.TableNextColumn(); - if (option is not MultiSubMod multi) - return; - - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority, - 50 * UiHelpers.Scale)) - panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority); - - ImGuiUtil.HoverTooltip("Option priority."); - } - - /// Draw the line to add a new option. - private static void DrawNewOption(ModPanelEditTab panel, int groupIdx, Vector2 iconButtonSize) - { - var mod = panel._mod; - var group = mod.Groups[groupIdx]; - var count = group switch - { - SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.OptionData.Count, - _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), - }; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{count + 1}"); - Target(panel, group, count); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; - if (ImGui.InputTextWithHint("##newOption", "Add new option...", ref tmp, 256)) - { - _newOptionName = tmp; - _newOptionNameIdx = groupIdx; - } - - ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; - var tt = canAddGroup - ? validName ? "Add a new option to this group." : "Please enter a name for the new option." - : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, - tt, !(canAddGroup && validName), true)) - return; - - panel._modManager.OptionEditor.AddOption(group, _newOptionName); - _newOptionName = string.Empty; - } - - // Handle drag and drop to move options inside a group or into another group. - private static void Source(IModOption option) - { - if (option.Group is not ITexToolsGroup) - return; - - using var source = ImRaii.DragDropSource(); - if (!source) - return; - - if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) - { - _dragDropGroup = option.Group; - _dragDropOption = option; - } - - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); - } - - private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx) - { - if (group is not ITexToolsGroup) - return; - - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) - return; - - if (_dragDropGroup != null && _dragDropOption != null) - { - if (_dragDropGroup == group) - { - var sourceOption = _dragDropOption; - panel._delayedActions.Enqueue( - () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceOption = _dragDropOption; - panel._delayedActions.Enqueue(() => - { - panel._modManager.OptionEditor.DeleteOption(sourceOption); - if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) - panel._modManager.OptionEditor.MoveOption(newOption, optionIdx); - }); - } - } - - _dragDropGroup = null; - _dragDropOption = null; - } - } - - /// Draw a combo to select single or multi group and switch between them. - private void DrawGroupCombo(IModGroup group, int groupIdx) - { - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); - using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); - if (!combo) - return; - - if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m) - _modManager.OptionEditor.MultiEditor.ChangeToSingle(m); - - var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); - if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s) - _modManager.OptionEditor.SingleEditor.ChangeToMulti(s); - - style.Pop(); - if (!canSwitchToMulti) - ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); - return; - - static string GroupTypeName(GroupType type) - => type switch - { - GroupType.Single => "Single Group", - GroupType.Multi => "Multi Group", - _ => "Unknown", - }; - } - /// Handles input text and integers in separate fields without buffers for every single one. private static class Input { @@ -705,6 +316,7 @@ public class ModPanelEditTab( { var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; ImGui.SetNextItemWidth(width); + if (ImGui.InputText(label, ref tmp, maxLength)) { _currentEdit = tmp; From df6eb3fdd2f4afac8b8b5e911b137ce0b9d54280 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 May 2024 18:30:40 +0200 Subject: [PATCH 085/865] Add some early support for IMC groups. --- OtterGui | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 4 +- .../Import/TexToolsMeta.Deserialization.cs | 2 +- .../Meta/Manipulations/ImcManipulation.cs | 4 +- .../Meta/Manipulations/MetaManipulation.cs | 4 +- Penumbra/Mods/Groups/ImcModGroup.cs | 43 ++++ .../Manager/OptionEditor/ImcModGroupEditor.cs | 33 ++- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/ModCreator.cs | 1 + Penumbra/Mods/SubMods/ImcSubMod.cs | 7 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 80 +----- Penumbra/UI/Classes/Combos.cs | 40 ++- Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 238 +++++++++++++++++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 42 +--- 15 files changed, 360 insertions(+), 146 deletions(-) diff --git a/OtterGui b/OtterGui index 866389b3..5028fba7 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 866389b3988d9c4926a786f6c78ac9d5265591ac +Subproject commit 5028fba767ca8febd75a1a5ebc312bd354efc81b diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index bc928360..843fe195 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -36,7 +36,7 @@ public readonly struct ImcCache : IDisposable public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip) { - if (!manip.Validate()) + if (!manip.Validate(true)) return false; var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); @@ -77,7 +77,7 @@ public readonly struct ImcCache : IDisposable public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m) { - if (!m.Validate()) + if (!m.Validate(false)) return false; var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 64eff8ba..325c9143 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -120,7 +120,7 @@ public partial class TexToolsMeta { var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value); - if (imc.Validate()) + if (imc.Validate(true)) MetaManipulations.Add(imc); } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index a1c4b5bf..45295990 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -146,7 +146,7 @@ public readonly struct ImcManipulation : IMetaManipulation public bool Apply(ImcFile file) => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); - public bool Validate() + public bool Validate(bool withMaterial) { switch (ObjectType) { @@ -178,7 +178,7 @@ public readonly struct ImcManipulation : IMetaManipulation break; } - if (Entry.MaterialId == 0) + if (withMaterial && Entry.MaterialId == 0) return false; return true; diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index e057d1a4..ed184d52 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -98,7 +98,7 @@ public readonly struct MetaManipulation : IEquatable, ICompara return; case ImcManipulation m: Imc = m; - ManipulationType = m.Validate() ? Type.Imc : Type.Unknown; + ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; return; } } @@ -108,7 +108,7 @@ public readonly struct MetaManipulation : IEquatable, ICompara { return ManipulationType switch { - Type.Imc => Imc.Validate(), + Type.Imc => Imc.Validate(true), Type.Eqdp => Eqdp.Validate(), Type.Eqp => Eqp.Validate(), Type.Est => Est.Validate(), diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e58d855a..21d8abe0 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,4 +1,7 @@ +using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -157,4 +160,44 @@ public class ImcModGroup(Mod mod) : IModGroup public (int Redirections, int Swaps, int Manips) GetCounts() => (0, 0, 1); + + public static ImcModGroup? Load(Mod mod, JObject json) + { + var options = json["Options"]; + var ret = new ImcModGroup(mod) + { + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, + ObjectType = json[nameof(ObjectType)]?.ToObject() ?? ObjectType.Unknown, + BodySlot = json[nameof(BodySlot)]?.ToObject() ?? BodySlot.Unknown, + EquipSlot = json[nameof(EquipSlot)]?.ToObject() ?? EquipSlot.Unknown, + PrimaryId = new PrimaryId(json[nameof(PrimaryId)]?.ToObject() ?? 0), + SecondaryId = new SecondaryId(json[nameof(SecondaryId)]?.ToObject() ?? 0), + Variant = new Variant(json[nameof(Variant)]?.ToObject() ?? 0), + CanBeDisabled = json[nameof(CanBeDisabled)]?.ToObject() ?? false, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + }; + if (ret.Name.Length == 0) + return null; + + if (options != null) + foreach (var child in options.Children()) + { + var subMod = new ImcSubMod(ret, child); + ret.OptionData.Add(subMod); + } + + if (!new ImcManipulation(ret.ObjectType, ret.BodySlot, ret.PrimaryId, ret.SecondaryId.Id, ret.Variant.Id, ret.EquipSlot, + ret.DefaultEntry).Validate(true)) + { + Penumbra.Messager.NotificationMessage($"Could not add IMC group because the associated IMC Entry is invalid.", + NotificationType.Warning); + return null; + } + + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + return ret; + } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 1194f961..f71547ba 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; +using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -11,13 +12,43 @@ namespace Penumbra.Mods.Manager.OptionEditor; public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) : ModOptionEditor(communicator, saveService, config), IService { + /// Add a new, empty imc group with the given manipulation data. + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcManipulation manip, SaveType saveType = SaveType.ImmediateSync) + { + if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) + return null; + + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + var group = CreateGroup(mod, newName, manip, maxPriority); + mod.Groups.Add(group); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); + return group; + } + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { - Name = newName, + Name = newName, Priority = priority, }; + + private static ImcModGroup CreateGroup(Mod mod, string newName, ImcManipulation manip, ModPriority priority, + SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + ObjectType = manip.ObjectType, + EquipSlot = manip.EquipSlot, + BodySlot = manip.BodySlot, + PrimaryId = manip.PrimaryId, + SecondaryId = manip.SecondaryId.Id, + Variant = manip.Variant, + DefaultEntry = manip.Entry, + }; + protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) => null; diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index b2b48ac0..3c00dcc1 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -246,7 +246,7 @@ public class ModGroupEditor( { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), _ => null, }; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 47261c6d..ed4245c4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -434,6 +434,7 @@ public partial class ModCreator( { case GroupType.Multi: return MultiModGroup.Load(mod, json); case GroupType.Single: return SingleModGroup.Load(mod, json); + case GroupType.Imc: return ImcModGroup.Load(mod, json); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index 167c8a6c..fca817aa 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using Penumbra.Mods.Groups; namespace Penumbra.Mods.SubMods; @@ -6,6 +7,12 @@ public class ImcSubMod(ImcModGroup group) : IModOption { public readonly ImcModGroup Group = group; + public ImcSubMod(ImcModGroup group, JToken json) + : this(group) + { + SubMod.LoadOptionData(json, this); + } + public Mod Mod => Group.Mod; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 4db0df47..6010cdaf 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -639,11 +639,11 @@ public class ItemSwapTab : IDisposable, ITab ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - _dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender); + _dirty |= Combos.Gender("##Gender", _currentGender, out _currentGender, InputWidth); if (drawRace == 1) { ImGui.SameLine(); - _dirty |= Combos.Race("##Race", InputWidth, _currentRace, out _currentRace); + _dirty |= Combos.Race("##Race", _currentRace, out _currentRace, InputWidth); } else if (drawRace == 2) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 743310ea..55125375 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -10,6 +10,7 @@ using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; @@ -145,7 +146,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip(ModelSetIdTooltip); ImGui.TableNextColumn(); - if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot)) + if (Combos.EqpEquipSlot("##eqpSlot", _new.Slot, out var slot)) _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId); ImGuiUtil.HoverTooltip(EquipSlotTooltip); @@ -351,90 +352,31 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - if (Combos.ImcType("##imcType", _new.ObjectType, out var type)) - { - var equipSlot = type switch - { - ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - _new = new ImcManipulation(type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? (ushort)1 : _new.SecondaryId, - _new.Variant.Id, equipSlot, _new.Entry); - } - - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); + var change = MetaManipulationDrawer.DrawObjectType(ref _new); ImGui.TableNextColumn(); - if (IdInput("##imcId", IdWidth, _new.PrimaryId.Id, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant.Id, _new.EquipSlot, _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(PrimaryIdTooltip); - + change |= MetaManipulationDrawer.DrawPrimaryId(ref _new); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. - if (_new.ObjectType is ObjectType.Equipment) - { - if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else if (_new.ObjectType is ObjectType.Accessory) - { - if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } + if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= MetaManipulationDrawer.DrawSlot(ref _new); else - { - if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId.Id, out var setId2, 0, ushort.MaxValue, false)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant.Id, _new.EquipSlot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } + change |= MetaManipulationDrawer.DrawSecondaryId(ref _new); ImGui.TableNextColumn(); - if (IdInput("##imcVariant", SmallIdWidth, _new.Variant.Id, out var variant, 0, byte.MaxValue, false)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, - _new.Entry).Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(VariantIdTooltip); + change |= MetaManipulationDrawer.DrawVariant(ref _new); ImGui.TableNextColumn(); if (_new.ObjectType is ObjectType.DemiHuman) - { - if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) - _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant.Id, slot, - _new.Entry) - .Copy(GetDefault(metaFileManager, _new) - ?? new ImcEntry()); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } + change |= MetaManipulationDrawer.DrawSlot(ref _new, 70); else - { ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); - } - + if (change) + _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); // Values using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 2cba7cf5..253bf0e0 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -8,41 +8,35 @@ namespace Penumbra.UI.Classes; public static class Combos { // Different combos to use with enums. - public static bool Race(string label, ModelRace current, out ModelRace race) - => Race(label, 100, current, out race); - - public static bool Race(string label, float unscaledWidth, ModelRace current, out ModelRace race) + public static bool Race(string label, ModelRace current, out ModelRace race, float unscaledWidth = 100) => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out race, RaceEnumExtensions.ToName, 1); - public static bool Gender(string label, Gender current, out Gender gender) - => Gender(label, 120, current, out gender); + public static bool Gender(string label, Gender current, out Gender gender, float unscaledWidth = 120) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, RaceEnumExtensions.ToName, 1); - public static bool Gender(string label, float unscaledWidth, Gender current, out Gender gender) - => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out gender, RaceEnumExtensions.ToName, 1); - - public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot) - => ImGuiUtil.GenericEnumCombo(label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, + public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName); - public static bool EqpEquipSlot(string label, float width, EquipSlot current, out EquipSlot slot) - => ImGuiUtil.GenericEnumCombo(label, width * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, + public static bool EqpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName); - public static bool AccessorySlot(string label, EquipSlot current, out EquipSlot slot) - => ImGuiUtil.GenericEnumCombo(label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, + public static bool AccessorySlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName); - public static bool SubRace(string label, SubRace current, out SubRace subRace) - => ImGuiUtil.GenericEnumCombo(label, 150 * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1); + public static bool SubRace(string label, SubRace current, out SubRace subRace, float unscaledWidth = 150) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1); - public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute) - => ImGuiUtil.GenericEnumCombo(label, 200 * UiHelpers.Scale, current, out attribute, + public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute, float unscaledWidth = 200) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute, RspAttributeExtensions.ToFullString, 0, 1); - public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute) - => ImGuiUtil.GenericEnumCombo(label, 200 * UiHelpers.Scale, current, out attribute); + public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute, float unscaledWidth = 200) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute); - public static bool ImcType(string label, ObjectType current, out ObjectType type) - => ImGuiUtil.GenericEnumCombo(label, 110 * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, + public static bool ImcType(string label, ObjectType current, out ObjectType type, float unscaledWidth = 110) + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, ObjectTypeExtensions.ToName); } diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index 5652fa98..b7262f95 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,12 +1,19 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; using ImGuiNET; +using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -15,9 +22,236 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; +using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.UI.ModsTab; +public static class MetaManipulationDrawer +{ + public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + { + var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, + manip.Variant.Id, equipSlot, manip.Entry); + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + manip.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (manip.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, + manip.Entry); + return ret; + } + + // A number input for ids with a optional max id of given width. + // Returns true if newId changed against currentId. + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } +} + +public class AddGroupDrawer : IUiService +{ + private string _groupName = string.Empty; + private bool _groupNameValid = false; + + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly MetaFileManager _metaManager; + private readonly ModManager _modManager; + + public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + { + _metaManager = metaManager; + _modManager = modManager; + UpdateEntry(); + } + + public void Draw(Mod mod, float width) + { + DrawBasicGroups(mod, width); + DrawImcData(mod, width); + } + + private void UpdateEntry() + { + try + { + _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, + out _entryExists); + _imcFileExists = true; + } + catch (Exception) + { + _defaultEntry = new ImcEntry(); + _imcFileExists = false; + _entryExists = false; + } + + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _entryInvalid = !_imcManip.Validate(true); + } + + + private void DrawBasicGroups(Mod mod, float width) + { + ImGui.SetNextItemWidth(width); + if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) + _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); + + var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); + if (ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid + ? "Add a new single selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + buttonWidth, !_groupNameValid)) + { + _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + ImUtf8.SameLineInner(); + if (ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid + ? "Add a new multi selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + buttonWidth, !_groupNameValid)) + { + _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + } + + private void DrawImcData(Mod mod, float width) + { + var halfWidth = (width - ImUtf8.ItemInnerSpacing.X) / 2 / ImUtf8.GlobalScale; + var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, halfWidth); + if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + { + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); + } + else if (_imcManip.ObjectType is ObjectType.DemiHuman) + { + var quarterWidth = (halfWidth - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + } + else + { + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, halfWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); + } + + if (change) + UpdateEntry(); + + var buttonWidth = new Vector2(halfWidth * ImUtf8.GlobalScale, 0); + + if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid + ? "Can not add a new group of this name."u8 + : _entryInvalid ? + "The associated IMC entry is invalid."u8 + : "Add a new multi selection option group to this mod."u8, + buttonWidth, !_groupNameValid || _entryInvalid)) + { + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); + _groupName = string.Empty; + _groupNameValid = false; + } + + if (_entryInvalid) + { + ImUtf8.SameLineInner(); + var text = _imcFileExists + ? "IMC Entry Does Not Exist" + : "IMC File Does Not Exist"; + ImGuiUtil.DrawTextButton(text, buttonWidth, Colors.PressEnterWarningBg); + } + } +} + public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, @@ -267,9 +501,7 @@ public sealed class ModGroupEditDrawer( } private void DrawImcGroup(ImcModGroup group) - { - // TODO - } + { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index a5db15b6..b7951c49 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -28,7 +28,8 @@ public class ModPanelEditTab( Configuration config, PredefinedTagManager predefinedTagManager, ModGroupEditDrawer groupEditDrawer, - DescriptionEditPopup descriptionPopup) + DescriptionEditPopup descriptionPopup, + AddGroupDrawer addGroupDrawer) : ITab { private readonly TagButtons _modTags = new(); @@ -75,7 +76,7 @@ public class ModPanelEditTab( selector.Selected!); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(filenames, modManager, _mod, config.ReplaceNonAsciiOnImport); + addGroupDrawer.Draw(_mod, UiHelpers.InputTextWidth.X); UiHelpers.DefaultLineSpace(); groupEditDrawer.Draw(_mod); @@ -84,7 +85,6 @@ public class ModPanelEditTab( public void Reset() { - AddOptionGroup.Reset(); MoveDirectory.Reset(); Input.Reset(); } @@ -202,42 +202,6 @@ public class ModPanelEditTab( Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } - /// Text input to add a new option group at the end of the current groups. - private static class AddOptionGroup - { - private static string _newGroupName = string.Empty; - - public static void Reset() - => _newGroupName = string.Empty; - - public static void Draw(FilenameService filenames, ModManager modManager, Mod mod, bool onlyAscii) - { - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); - ImGui.SameLine(); - var defaultFile = filenames.OptionGroupFile(mod, -1, onlyAscii); - var fileExists = File.Exists(defaultFile); - var tt = fileExists - ? "Open the default option json file in the text editor of your choice." - : "The default option json file does not exist."; - if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", UiHelpers.IconButtonSize, tt, - !fileExists, true)) - Process.Start(new ProcessStartInfo(defaultFile) { UseShellExecute = true }); - - ImGui.SameLine(); - - var nameValid = ModGroupEditor.VerifyFileName(mod, null, _newGroupName, false); - tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, - tt, !nameValid, true)) - return; - - modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName); - Reset(); - } - } - /// A text input for the new directory name and a button to apply the move. private static class MoveDirectory { From bb56faa288be5bf200e0e44af93a649b382ef632 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 May 2024 18:28:40 +0200 Subject: [PATCH 086/865] Improvements. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 27 +- Penumbra/Mods/Manager/ModCacheManager.cs | 3 + .../Manager/OptionEditor/ImcAttributeCache.cs | 123 ++++++++ .../Manager/OptionEditor/ImcModGroupEditor.cs | 63 ++++ Penumbra/Mods/SubMods/ImcSubMod.cs | 7 +- Penumbra/UI/ModsTab/AddGroupDrawer.cs | 161 ++++++++++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 280 +++++++++--------- 9 files changed, 498 insertions(+), 170 deletions(-) create mode 100644 Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs create mode 100644 Penumbra/UI/ModsTab/AddGroupDrawer.cs diff --git a/OtterGui b/OtterGui index 5028fba7..462acb87 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5028fba767ca8febd75a1a5ebc312bd354efc81b +Subproject commit 462acb87099650019996e4306d18cc70f76ca576 diff --git a/Penumbra.GameData b/Penumbra.GameData index 595ac572..5fa4d0e7 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 595ac5722c9c400bea36110503ed2ae7b02d1489 +Subproject commit 5fa4d0e7972423b73f8cf569bb2bfbeddd825c8a diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 21d8abe0..671d684f 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -14,8 +14,7 @@ namespace Penumbra.Mods.Groups; public class ImcModGroup(Mod mod) : IModGroup { - public const int DisabledIndex = 30; - public const int NumAttributes = 10; + public const int DisabledIndex = 60; public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; @@ -55,23 +54,20 @@ public class ImcModGroup(Mod mod) : IModGroup } } + public bool DefaultDisabled + => _canBeDisabled && DefaultSettings.HasFlag(DisabledIndex); + public IModOption? AddOption(string name, string description = "") { - uint fullMask = GetFullMask(); - var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask); - // All attributes handled. - if (firstUnset >= NumAttributes) - return null; - var groupIdx = Mod.Groups.IndexOf(this); if (groupIdx < 0) return null; var subMod = new ImcSubMod(this) { - Name = name, - Description = description, - AttributeIndex = firstUnset, + Name = name, + Description = description, + AttributeMask = 0, }; OptionData.Add(subMod); return subMod; @@ -100,7 +96,7 @@ public class ImcModGroup(Mod mod) : IModGroup continue; var option = OptionData[i]; - mask |= option.Attribute; + mask |= option.AttributeMask; } return mask; @@ -109,11 +105,10 @@ public class ImcModGroup(Mod mod) : IModGroup private ushort GetFullMask() => GetCurrentMask(Setting.AllBits(63)); - private ImcManipulation GetManip(ushort mask) + public ImcManipulation GetManip(ushort mask) => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, DefaultEntry with { AttributeMask = mask }); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { if (CanBeDisabled && setting.HasFlag(DisabledIndex)) @@ -150,8 +145,8 @@ public class ImcModGroup(Mod mod) : IModGroup { jWriter.WriteStartObject(); SubMod.WriteModOption(jWriter, option); - jWriter.WritePropertyName(nameof(option.AttributeIndex)); - jWriter.WriteValue(option.AttributeIndex); + jWriter.WritePropertyName(nameof(option.AttributeMask)); + jWriter.WriteValue(option.AttributeMask); jWriter.WriteEndObject(); } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 59c88cf0..c6a723a0 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -202,6 +202,9 @@ public class ModCacheManager : IDisposable foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier, changedItems, manip); + foreach(var imcGroup in mod.Groups.OfType()) + ComputeChangedItems(_identifier, changedItems, imcGroup.GetManip(0)); + mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs new file mode 100644 index 00000000..e1235c5b --- /dev/null +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -0,0 +1,123 @@ +using OtterGui; +using Penumbra.GameData.Structs; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Mods.Manager.OptionEditor; + +public unsafe ref struct ImcAttributeCache +{ + private fixed bool _canChange[ImcEntry.NumAttributes]; + private fixed byte _option[ImcEntry.NumAttributes]; + + /// Obtain the earliest unset flag, or 0 if none are unset. + public readonly ushort LowestUnsetMask; + + public ImcAttributeCache(ImcModGroup group) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + _canChange[i] = true; + _option[i] = byte.MaxValue; + + var flag = (ushort)(1 << i); + var set = (group.DefaultEntry.AttributeMask & flag) != 0; + if (set) + { + _canChange[i] = true; + _option[i] = byte.MaxValue - 1; + continue; + } + + foreach (var (option, idx) in group.OptionData.WithIndex()) + { + set = (option.AttributeMask & flag) != 0; + if (set) + { + _canChange[i] = option.AttributeMask != flag; + _option[i] = (byte)idx; + break; + } + } + + if (_option[i] == byte.MaxValue && LowestUnsetMask is 0) + LowestUnsetMask = flag; + } + } + + + /// Checks whether an attribute flag can be set by anything, i.e. if it might be the only flag for an option and thus could not be removed from that option. + public readonly bool CanChange(int idx) + => _canChange[idx]; + + /// Set a default attribute flag to a value if possible, remove it from its prior option if necessary, and return if anything changed. + public readonly bool Set(ImcModGroup group, int idx, bool value) + { + var flag = 1 << idx; + var oldMask = group.DefaultEntry.AttributeMask; + if (!value) + { + var newMask = (ushort)(oldMask & ~flag); + if (oldMask == newMask) + return false; + + group.DefaultEntry = group.DefaultEntry with { AttributeMask = newMask }; + return true; + } + + if (!_canChange[idx]) + return false; + + var mask = (ushort)(oldMask | flag); + if (oldMask == mask) + return false; + + group.DefaultEntry = group.DefaultEntry with { AttributeMask = mask }; + if (_option[idx] <= ImcEntry.NumAttributes) + { + var option = group.OptionData[_option[idx]]; + option.AttributeMask = (ushort)(option.AttributeMask & ~flag); + } + + return true; + } + + /// Set an attribute flag to a value if possible, remove it from its prior option or the default entry if necessary, and return if anything changed. + public readonly bool Set(ImcSubMod option, int idx, bool value) + { + if (!_canChange[idx]) + return false; + + var flag = 1 << idx; + var oldMask = option.AttributeMask; + if (!value) + { + var newMask = (ushort)(oldMask & ~flag); + if (oldMask == newMask) + return false; + + option.AttributeMask = newMask; + return true; + } + + var mask = (ushort)(oldMask | flag); + if (oldMask == mask) + return false; + + option.AttributeMask = mask; + if (_option[idx] <= ImcEntry.NumAttributes) + { + var oldOption = option.Group.OptionData[_option[idx]]; + oldOption.AttributeMask = (ushort)(oldOption.AttributeMask & ~flag); + } + else if (_option[idx] is byte.MaxValue - 1) + { + option.Group.DefaultEntry = option.Group.DefaultEntry with + { + AttributeMask = (ushort)(option.Group.DefaultEntry.AttributeMask & ~flag), + }; + } + + return true; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index f71547ba..20021d29 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -1,11 +1,13 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; +using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; namespace Penumbra.Mods.Manager.OptionEditor; @@ -26,6 +28,67 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ return group; } + public ImcSubMod? AddOption(ImcModGroup group, in ImcAttributeCache cache, string name, string description = "", + SaveType saveType = SaveType.Queue) + { + if (cache.LowestUnsetMask == 0) + return null; + + var subMod = new ImcSubMod(group) + { + Name = name, + Description = description, + AttributeMask = cache.LowestUnsetMask, + }; + group.OptionData.Add(subMod); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, subMod, null, -1); + return subMod; + } + + // Hide this method. + private new ImcSubMod? AddOption(ImcModGroup group, string name, SaveType saveType) + => null; + + public void ChangeDefaultAttribute(ImcModGroup group, in ImcAttributeCache cache, int idx, bool value, SaveType saveType = SaveType.Queue) + { + if (!cache.Set(group, idx, value)) + return; + + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + + public void ChangeDefaultEntry(ImcModGroup group, in ImcEntry newEntry, SaveType saveType = SaveType.Queue) + { + var entry = newEntry with { AttributeMask = group.DefaultEntry.AttributeMask }; + if (entry.MaterialId == 0 || group.DefaultEntry.Equals(entry)) + return; + + group.DefaultEntry = entry; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + + public void ChangeOptionAttribute(ImcSubMod option, in ImcAttributeCache cache, int idx, bool value, SaveType saveType = SaveType.Queue) + { + if (!cache.Set(option, idx, value)) + return; + + SaveService.Save(saveType, new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, option.Mod, option.Group, option, null, -1); + } + + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) + { + if (group.CanBeDisabled == canBeDisabled) + return; + + group.CanBeDisabled = canBeDisabled; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index fca817aa..7f46bc95 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; namespace Penumbra.Mods.SubMods; @@ -11,15 +12,13 @@ public class ImcSubMod(ImcModGroup group) : IModOption : this(group) { SubMod.LoadOptionData(json, this); + AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); } public Mod Mod => Group.Mod; - public byte AttributeIndex; - - public ushort Attribute - => (ushort)(1 << AttributeIndex); + public ushort AttributeMask; Mod IModOption.Mod => Mod; diff --git a/Penumbra/UI/ModsTab/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/AddGroupDrawer.cs new file mode 100644 index 00000000..79c1bf9d --- /dev/null +++ b/Penumbra/UI/ModsTab/AddGroupDrawer.cs @@ -0,0 +1,161 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public class AddGroupDrawer : IUiService +{ + private string _groupName = string.Empty; + private bool _groupNameValid = false; + + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly MetaFileManager _metaManager; + private readonly ModManager _modManager; + + public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + { + _metaManager = metaManager; + _modManager = modManager; + UpdateEntry(); + } + + public void Draw(Mod mod, float width) + { + var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); + DrawBasicGroups(mod, width, buttonWidth); + DrawImcData(mod, buttonWidth); + } + + private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth) + { + ImGui.SetNextItemWidth(width); + if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) + _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); + + DrawSingleGroupButton(mod, buttonWidth); + ImUtf8.SameLineInner(); + DrawMultiGroupButton(mod, buttonWidth); + } + + private void DrawSingleGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid + ? "Add a new single selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + private void DrawMultiGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid + ? "Add a new multi selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + private void DrawImcInput(float width) + { + var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, width); + if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + { + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + } + else if (_imcManip.ObjectType is ObjectType.DemiHuman) + { + var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + } + else + { + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + } + + if (change) + UpdateEntry(); + } + + private void DrawImcData(Mod mod, Vector2 width) + { + var halfWidth = width.X / ImUtf8.GlobalScale; + DrawImcInput(halfWidth); + DrawImcButton(mod, width); + } + + private void DrawImcButton(Mod mod, Vector2 width) + { + if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid + ? "Can not add a new group of this name."u8 + : _entryInvalid + ? "The associated IMC entry is invalid."u8 + : "Add a new multi selection option group to this mod."u8, + width, !_groupNameValid || _entryInvalid)) + { + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); + _groupName = string.Empty; + _groupNameValid = false; + } + + if (_entryInvalid) + { + ImUtf8.SameLineInner(); + var text = _imcFileExists + ? "IMC Entry Does Not Exist"u8 + : "IMC File Does Not Exist"u8; + ImUtf8.TextFramed(text, Colors.PressEnterWarningBg, width); + } + } + + private void UpdateEntry() + { + try + { + _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, + out _entryExists); + _imcFileExists = true; + } + catch (Exception) + { + _defaultEntry = new ImcEntry(); + _imcFileExists = false; + _entryExists = false; + } + + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _entryInvalid = !_imcManip.Validate(true); + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index b7262f95..a94c25ea 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,15 +1,12 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; -using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; -using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -22,7 +19,6 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; -using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.UI.ModsTab; @@ -104,8 +100,10 @@ public static class MetaManipulationDrawer return ret; } - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, bool border) { @@ -121,142 +119,12 @@ public static class MetaManipulationDrawer } } -public class AddGroupDrawer : IUiService -{ - private string _groupName = string.Empty; - private bool _groupNameValid = false; - - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; - private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; - - public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) - { - _metaManager = metaManager; - _modManager = modManager; - UpdateEntry(); - } - - public void Draw(Mod mod, float width) - { - DrawBasicGroups(mod, width); - DrawImcData(mod, width); - } - - private void UpdateEntry() - { - try - { - _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, - out _entryExists); - _imcFileExists = true; - } - catch (Exception) - { - _defaultEntry = new ImcEntry(); - _imcFileExists = false; - _entryExists = false; - } - - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); - _entryInvalid = !_imcManip.Validate(true); - } - - - private void DrawBasicGroups(Mod mod, float width) - { - ImGui.SetNextItemWidth(width); - if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) - _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); - - var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); - if (ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid - ? "Add a new single selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - buttonWidth, !_groupNameValid)) - { - _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; - _groupNameValid = false; - } - - ImUtf8.SameLineInner(); - if (ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid - ? "Add a new multi selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - buttonWidth, !_groupNameValid)) - { - _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; - _groupNameValid = false; - } - } - - private void DrawImcData(Mod mod, float width) - { - var halfWidth = (width - ImUtf8.ItemInnerSpacing.X) / 2 / ImUtf8.GlobalScale; - var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, halfWidth); - if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) - { - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); - } - else if (_imcManip.ObjectType is ObjectType.DemiHuman) - { - var quarterWidth = (halfWidth - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); - } - else - { - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); - } - - if (change) - UpdateEntry(); - - var buttonWidth = new Vector2(halfWidth * ImUtf8.GlobalScale, 0); - - if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid - ? "Can not add a new group of this name."u8 - : _entryInvalid ? - "The associated IMC entry is invalid."u8 - : "Add a new multi selection option group to this mod."u8, - buttonWidth, !_groupNameValid || _entryInvalid)) - { - _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; - _groupNameValid = false; - } - - if (_entryInvalid) - { - ImUtf8.SameLineInner(); - var text = _imcFileExists - ? "IMC Entry Does Not Exist" - : "IMC File Does Not Exist"; - ImGuiUtil.DrawTextButton(text, buttonWidth, Colors.PressEnterWarningBg); - } - } -} - public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService + DescriptionEditPopup descriptionPopup, + MetaFileManager metaManager) : IUiService { private static ReadOnlySpan DragDropLabel => "##DragOption"u8; @@ -501,7 +369,111 @@ public sealed class ModGroupEditDrawer( } private void DrawImcGroup(ImcModGroup group) - { } + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Object Type"u8); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text("Slot"u8); + ImUtf8.Text("Primary ID"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text("Secondary ID"); + ImUtf8.Text("Variant"u8); + + ImUtf8.TextFrameAligned("Material ID"u8); + ImUtf8.TextFrameAligned("Material Animation ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Sound ID"u8); + ImUtf8.TextFrameAligned("Can Be Disabled"u8); + ImUtf8.TextFrameAligned("Default Attributes"u8); + } + + ImGui.SameLine(); + + var attributeCache = new ImcAttributeCache(group); + + using (ImUtf8.Group()) + { + ImUtf8.Text(group.ObjectType.ToName()); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text(group.EquipSlot.ToName()); + ImUtf8.Text($"{group.PrimaryId.Id}"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text($"{group.SecondaryId.Id}"); + ImUtf8.Text($"{group.Variant.Id}"); + + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); + + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + modManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); + + var defaultDisabled = group.DefaultDisabled; + ImUtf8.SameLineInner(); + if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, + group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); + + DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + } + + + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionName(option); + + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(_priorityWidth, 0)); + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + _optionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); + DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + + DrawNewOption(group, attributeCache); + return; + + static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & (1 << i)) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip($"{(char)('A' + i)}"); + if (i != 9) + ImUtf8.SameLineInner(); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) @@ -575,14 +547,14 @@ public sealed class ModGroupEditDrawer( if (count >= int.MaxValue) return; - DrawNewOptionBase(group, count); + var name = DrawNewOptionBase(group, count); - var validName = _newOptionName?.Length > 0; + var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8, !validName)) { - modManager.OptionEditor.SingleEditor.AddOption(group, _newOptionName!); + modManager.OptionEditor.SingleEditor.AddOption(group, name); _newOptionName = null; } } @@ -593,25 +565,36 @@ public sealed class ModGroupEditDrawer( if (count >= IModGroup.MaxMultiOptions) return; - DrawNewOptionBase(group, count); + var name = DrawNewOptionBase(group, count); - var validName = _newOptionName?.Length > 0; + var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8, !validName)) { - modManager.OptionEditor.MultiEditor.AddOption(group, _newOptionName!); + modManager.OptionEditor.MultiEditor.AddOption(group, name); _newOptionName = null; } } - private void DrawNewOption(ImcModGroup group) + private void DrawNewOption(ImcModGroup group, in ImcAttributeCache cache) { - // TODO + if (cache.LowestUnsetMask == 0) + return; + + var name = DrawNewOptionBase(group, group.Options.Count); + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.ImcEditor.AddOption(group, cache, name); + _newOptionName = null; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawNewOptionBase(IModGroup group, int count) + private string DrawNewOptionBase(IModGroup group, int count) { ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); Target(group, count); @@ -631,6 +614,7 @@ public sealed class ModGroupEditDrawer( } ImUtf8.SameLineInner(); + return newName; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From e85b84dafe543b3a2e8bcfe13ed665fbe92b4c93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 May 2024 18:24:21 +0200 Subject: [PATCH 087/865] Add the option to omit mch offhands from changed items. --- Penumbra/Collections/Cache/CollectionCache.cs | 10 +- .../Cache/CollectionCacheManager.cs | 8 +- Penumbra/Configuration.cs | 25 ++-- Penumbra/Mods/Groups/IModGroup.cs | 2 + Penumbra/Mods/Groups/ImcModGroup.cs | 5 + Penumbra/Mods/Groups/MultiModGroup.cs | 8 ++ Penumbra/Mods/Groups/SingleModGroup.cs | 8 ++ Penumbra/Mods/Manager/ModCacheManager.cs | 92 ++------------ Penumbra/Mods/Manager/ModImportManager.cs | 19 +-- Penumbra/Mods/Mod.cs | 6 +- Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 8 ++ Penumbra/Util/IdentifierExtensions.cs | 115 ++++++++++++++++++ 13 files changed, 192 insertions(+), 117 deletions(-) create mode 100644 Penumbra/Util/IdentifierExtensions.cs diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index ded1dc73..e2f20b46 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Mods.Manager; +using Penumbra.Util; namespace Penumbra.Collections.Cache; @@ -252,8 +253,8 @@ public sealed class CollectionCache : IDisposable return mod.GetData(); var settings = _collection[mod.Index].Settings; - return settings is not { Enabled: true } - ? AppliedModData.Empty + return settings is not { Enabled: true } + ? AppliedModData.Empty : mod.GetData(settings); } @@ -439,9 +440,12 @@ public sealed class CollectionCache : IDisposable foreach (var (manip, mod) in Meta) { - ModCacheManager.ComputeChangedItems(identifier, items, manip); + identifier.MetaChangedItems(items, manip); AddItems(mod); } + + if (_manager.Config.HideMachinistOffhandFromChangedItems) + _changedItems.RemoveMachinistOffhands(); } catch (Exception e) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index fb9ee9a3..ca57c8b9 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -25,6 +25,7 @@ public class CollectionCacheManager : IDisposable private readonly ModStorage _modStorage; private readonly CollectionStorage _storage; private readonly ActiveCollections _active; + internal readonly Configuration Config; internal readonly ResolvedFileChanged ResolvedFileChanged; internal readonly MetaFileManager MetaFileManager; internal readonly ResourceLoader ResourceLoader; @@ -40,7 +41,8 @@ public class CollectionCacheManager : IDisposable => _storage.Where(c => c.HasCache); public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, - MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader) + MetaFileManager metaFileManager, ActiveCollections active, CollectionStorage storage, ResourceLoader resourceLoader, + Configuration config) { _framework = framework; _communicator = communicator; @@ -50,6 +52,7 @@ public class CollectionCacheManager : IDisposable _active = active; _storage = storage; ResourceLoader = resourceLoader; + Config = config; ResolvedFileChanged = _communicator.ResolvedFileChanged; if (!_active.Individuals.IsLoaded) @@ -260,7 +263,8 @@ public class CollectionCacheManager : IDisposable } /// Prepare Changes by removing mods from caches with collections or add or reload mods. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int movedToIdx) { if (type is ModOptionChangeType.PrepareChange) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a065bc26..b81e84d8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -41,18 +41,19 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; + public int OptionGroupCollapsibleMin { get; set; } = 5; public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY); diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 2ec60f7e..ab367532 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -40,6 +41,7 @@ public interface IModGroup public int GetIndex(); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 671d684f..173bf57e 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -3,12 +3,14 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -119,6 +121,9 @@ public class ImcModGroup(Mod mod) : IModGroup manipulations.Add(imc); } + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.MetaChangedItems(changedItems, GetManip(0)); + public Setting FixSetting(Setting setting) => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7495c4b4..f587fc8f 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -4,10 +4,12 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; 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.Util; namespace Penumbra.Mods.Groups; @@ -114,6 +116,12 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in DataContainers) + identifier.AddChangedItems(container, changedItems); + } + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) { ModSaveGroup.WriteJsonBase(jWriter, this); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index bc463c1e..7a551322 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -2,10 +2,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; 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.Util; namespace Penumbra.Mods.Groups; @@ -99,6 +101,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in DataContainers) + identifier.AddChangedItems(container, changedItems); + } + public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index c6a723a0..8ab8cf33 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,26 +1,27 @@ using Penumbra.Communication; using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Mods.Manager; public class ModCacheManager : IDisposable { + private readonly Configuration _config; private readonly CommunicatorService _communicator; private readonly ObjectIdentification _identifier; private readonly ModStorage _modManager; private bool _updatingItems = false; - public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage) + public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config) { _communicator = communicator; _identifier = identifier; _modManager = modStorage; + _config = config; _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager); @@ -38,75 +39,8 @@ public class ModCacheManager : IDisposable _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); } - /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. - public static void ComputeChangedItems(ObjectIdentification identifier, IDictionary changedItems, MetaManipulation manip) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - switch (manip.Imc.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - identifier.Identify(changedItems, - GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Weapon: - identifier.Identify(changedItems, - GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - case ObjectType.DemiHuman: - identifier.Identify(changedItems, - GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Monster: - identifier.Identify(changedItems, - GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - } - - break; - case MetaManipulation.Type.Eqdp: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); - break; - case MetaManipulation.Type.Eqp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); - break; - case MetaManipulation.Type.Est: - switch (manip.Est.Slot) - { - case EstManipulation.EstType.Hair: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); - break; - case EstManipulation.EstType.Face: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); - break; - case EstManipulation.EstType.Body: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Body)); - break; - case EstManipulation.EstType.Head: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Head)); - break; - } - - break; - case MetaManipulation.Type.Gmp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); - break; - case MetaManipulation.Type.Rsp: - changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); - break; - } - } - - private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int fromIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int fromIdx) { switch (type) { @@ -194,16 +128,14 @@ public class ModCacheManager : IDisposable private void UpdateChangedItems(Mod mod) { - var changedItems = (SortedList)mod.ChangedItems; - changedItems.Clear(); - foreach (var gamePath in mod.AllDataContainers.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) - _identifier.Identify(changedItems, gamePath.ToString()); + mod.ChangedItems.Clear(); - foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) - ComputeChangedItems(_identifier, changedItems, manip); + _identifier.AddChangedItems(mod.Default, mod.ChangedItems); + foreach (var group in mod.Groups) + group.AddChangedItems(_identifier, mod.ChangedItems); - foreach(var imcGroup in mod.Groups.OfType()) - ComputeChangedItems(_identifier, changedItems, imcGroup.GetManip(0)); + if (_config.HideMachinistOffhandFromChangedItems) + mod.ChangedItems.RemoveMachinistOffhands(); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); } diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 73571ea4..c99b7d0e 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -5,12 +5,8 @@ using Penumbra.Mods.Editor; namespace Penumbra.Mods.Manager; -public class ModImportManager : IDisposable +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable { - private readonly ModManager _modManager; - private readonly Configuration _config; - private readonly ModEditor _modEditor; - private readonly ConcurrentQueue _modsToUnpack = new(); /// Mods need to be added thread-safely outside of iteration. @@ -26,13 +22,6 @@ public class ModImportManager : IDisposable => _modsToAdd; - public ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) - { - _modManager = modManager; - _config = config; - _modEditor = modEditor; - } - public void TryUnpacking() { if (Importing || !_modsToUnpack.TryDequeue(out var newMods)) @@ -51,7 +40,7 @@ public class ModImportManager : IDisposable if (files.Length == 0) return; - _import = new TexToolsImporter(files.Length, files, AddNewMod, _config, _modEditor, _modManager, _modEditor.Compactor); + _import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor); } public bool Importing @@ -87,8 +76,8 @@ public class ModImportManager : IDisposable return false; } - _modManager.AddMod(directory); - mod = _modManager.LastOrDefault(); + modManager.AddMod(directory); + mod = modManager.LastOrDefault(); return mod != null && mod.ModPath == directory; } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 6f6eb8ce..783ef3e6 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -62,8 +62,8 @@ public sealed class Mod : IMod // Options - public readonly DefaultSubMod Default; - public readonly List Groups = []; + public readonly DefaultSubMod Default; + public readonly List Groups = []; public AppliedModData GetData(ModSettings? settings = null) { @@ -99,7 +99,7 @@ public sealed class Mod : IMod } // Cache - public readonly IReadOnlyDictionary ChangedItems = new SortedList(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index a94c25ea..4ef1577f 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -123,8 +123,7 @@ public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup, - MetaFileManager metaManager) : IUiService + DescriptionEditPopup descriptionPopup) : IUiService { private static ReadOnlySpan DragDropLabel => "##DragOption"u8; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 439f7be4..30384538 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -428,6 +428,14 @@ public class SettingsTab : ITab _config.Ephemeral.Save(); } }); + Checkbox("Omit Machinist Offhands in Changed Items", + "Omits all Aetherotransformers (machinist offhands) in the changed items tabs because any change on them changes all of them at the moment.\n\n" + + "Changing this triggers a rediscovery of your mods so all changed items can be updated.", + _config.HideMachinistOffhandFromChangedItems, v => + { + _config.HideMachinistOffhandFromChangedItems = v; + _modManager.DiscoverMods(); + }); Checkbox("Hide Priority Numbers in Mod Selector", "Hides the bracketed non-zero priority numbers displayed in the mod selector when there is enough space for them.", _config.HidePrioritiesInSelector, v => _config.HidePrioritiesInSelector = v); diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs new file mode 100644 index 00000000..392a5aba --- /dev/null +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -0,0 +1,115 @@ +using OtterGui.Classes; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Util; + +public static class IdentifierExtensions +{ + /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. + public static void MetaChangedItems(this ObjectIdentification identifier, IDictionary changedItems, + MetaManipulation manip) + { + switch (manip.ManipulationType) + { + case MetaManipulation.Type.Imc: + switch (manip.Imc.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + identifier.Identify(changedItems, + GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, + "a")); + break; + case ObjectType.Weapon: + identifier.Identify(changedItems, + GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); + break; + case ObjectType.DemiHuman: + identifier.Identify(changedItems, + GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, + "a")); + break; + case ObjectType.Monster: + identifier.Identify(changedItems, + GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); + break; + } + + break; + case MetaManipulation.Type.Eqdp: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); + break; + case MetaManipulation.Type.Eqp: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); + break; + case MetaManipulation.Type.Est: + switch (manip.Est.Slot) + { + case EstManipulation.EstType.Hair: + changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); + break; + case EstManipulation.EstType.Face: + changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); + break; + case EstManipulation.EstType.Body: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), + EquipSlot.Body)); + break; + case EstManipulation.EstType.Head: + identifier.Identify(changedItems, + GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), + EquipSlot.Head)); + break; + } + + break; + case MetaManipulation.Type.Gmp: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + break; + case MetaManipulation.Type.Rsp: + changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); + break; + } + } + + public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, + IDictionary changedItems) + { + foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) + identifier.Identify(changedItems, gamePath.ToString()); + + foreach (var manip in container.Manipulations) + MetaChangedItems(identifier, changedItems, manip); + } + + public static void RemoveMachinistOffhands(this SortedList changedItems) + { + for (var i = 0; i < changedItems.Count; i++) + { + { + var value = changedItems.Values[i]; + if (value is EquipItem { Type: FullEquipType.GunOff }) + changedItems.RemoveAt(i--); + } + } + } + + public static void RemoveMachinistOffhands(this SortedList, object?)> changedItems) + { + for (var i = 0; i < changedItems.Count; i++) + { + { + var value = changedItems.Values[i].Item2; + if (value is EquipItem { Type: FullEquipType.GunOff }) + changedItems.RemoveAt(i--); + } + } + } +} From 2585de8b21e25fd62cf2295d58ac2520e526752b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 21 May 2024 22:01:20 +0200 Subject: [PATCH 088/865] Cleanup group drawing somewhat. --- Penumbra/Mods/Editor/ModNormalizer.cs | 16 +- Penumbra/Mods/Groups/IModGroup.cs | 3 + Penumbra/Mods/Groups/ImcModGroup.cs | 5 + Penumbra/Mods/Groups/MultiModGroup.cs | 4 + Penumbra/Mods/Groups/SingleModGroup.cs | 4 + .../Manager/OptionEditor/ModGroupEditor.cs | 94 +-- .../UI/ModsTab/{ => Groups}/AddGroupDrawer.cs | 32 +- .../UI/ModsTab/Groups/IModGroupEditDrawer.cs | 6 + .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 138 ++++ .../UI/ModsTab/{ => Groups}/ModGroupDrawer.cs | 36 +- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 360 +++++++++ .../ModsTab/Groups/MultiModGroupEditDrawer.cs | 63 ++ .../Groups/SingleModGroupEditDrawer.cs | 68 ++ Penumbra/UI/ModsTab/MetaManipulationDrawer.cs | 105 +++ Penumbra/UI/ModsTab/ModGroupEditDrawer.cs | 687 ------------------ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 1 + 17 files changed, 841 insertions(+), 782 deletions(-) rename Penumbra/UI/ModsTab/{ => Groups}/AddGroupDrawer.cs (83%) create mode 100644 Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs rename Penumbra/UI/ModsTab/{ => Groups}/ModGroupDrawer.cs (85%) create mode 100644 Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs create mode 100644 Penumbra/UI/ModsTab/MetaManipulationDrawer.cs delete mode 100644 Penumbra/UI/ModsTab/ModGroupEditDrawer.cs diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 437600c9..58e4fc08 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -279,19 +278,8 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) private void ApplyRedirections() { foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) - { - switch (group) - { - case SingleModGroup single: - foreach (var (option, optionIdx) in single.OptionData.WithIndex()) - _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); - break; - case MultiModGroup multi: - foreach (var (option, optionIdx) in multi.OptionData.WithIndex()) - _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); - break; - } - } + foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) + _modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); ++Step; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index ab367532..fcc8c093 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -5,6 +5,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.ModsTab.Groups; namespace Penumbra.Mods.Groups; @@ -40,6 +41,8 @@ public interface IModGroup public int GetIndex(); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 173bf57e..d2c41f34 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -10,6 +10,8 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.ModsTab; +using Penumbra.UI.ModsTab.Groups; using Penumbra.Util; namespace Penumbra.Mods.Groups; @@ -89,6 +91,9 @@ public class ImcModGroup(Mod mod) : IModGroup public int GetIndex() => ModGroup.GetIndex(this); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new ImcModGroupEditDrawer(editDrawer, this); + private ushort GetCurrentMask(Setting setting) { var mask = DefaultEntry.AttributeMask; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index f587fc8f..7fc9acb3 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -9,6 +9,7 @@ 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; @@ -107,6 +108,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public int GetIndex() => ModGroup.GetIndex(this); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new MultiModGroupEditDrawer(editDrawer, this); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 7a551322..4eec0746 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -7,6 +7,7 @@ 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; @@ -93,6 +94,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public int GetIndex() => ModGroup.GetIndex(this); + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new SingleModGroupEditDrawer(editDrawer, this); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { if (OptionData.Count == 0) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 3c00dcc1..969ad3fa 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -37,8 +37,8 @@ public class ModGroupEditor( SingleModGroupEditor singleEditor, MultiModGroupEditor multiEditor, ImcModGroupEditor imcEditor, - CommunicatorService Communicator, - SaveService SaveService, + CommunicatorService communicator, + SaveService saveService, Configuration Config) : IService { public SingleModGroupEditor SingleEditor @@ -57,8 +57,8 @@ public class ModGroupEditor( return; group.DefaultSettings = defaultOption; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); + saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); } /// Rename an option group if possible. @@ -68,10 +68,10 @@ public class ModGroupEditor( if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) return; - SaveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); group.Name = newName; - SaveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); + saveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); } /// Delete a given option group. Fires an event to prepare before actually deleting. @@ -79,22 +79,22 @@ public class ModGroupEditor( { var mod = group.Mod; var idx = group.GetIndex(); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); mod.Groups.RemoveAt(idx); - SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); + saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); } /// Move the index of a given option group. public void MoveModGroup(IModGroup group, int groupIdxTo) { - var mod = group.Mod; + var mod = group.Mod; var idxFrom = group.GetIndex(); if (!mod.Groups.Move(idxFrom, groupIdxTo)) return; - SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); + saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); } /// Change the internal priority of the given option group. @@ -104,8 +104,8 @@ public class ModGroupEditor( return; group.Priority = newPriority; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); + saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); } /// Change the description of the given option group. @@ -115,8 +115,8 @@ public class ModGroupEditor( return; group.Description = newDescription; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); + saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); } /// Rename the given option. @@ -126,8 +126,8 @@ public class ModGroupEditor( return; option.Name = newName; - SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } /// Change the description of the given option. @@ -137,8 +137,8 @@ public class ModGroupEditor( return; option.Description = newDescription; - SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } /// Set the meta manipulations for a given option. Replaces existing manipulations. @@ -148,10 +148,10 @@ public class ModGroupEditor( && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Manipulations.SetTo(manipulations); - SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Set the file redirections for a given option. Replaces existing redirections. @@ -160,10 +160,10 @@ public class ModGroupEditor( if (subMod.Files.SetEquals(replacements)) return; - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Files.SetTo(replacements); - SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. @@ -173,8 +173,8 @@ public class ModGroupEditor( subMod.Files.AddFrom(additions); if (oldCount != subMod.Files.Count) { - SaveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } } @@ -184,10 +184,10 @@ public class ModGroupEditor( if (subMod.FileSwaps.SetEquals(swaps)) return; - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.FileSwaps.SetTo(swaps); - SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Verify that a new option group name is unique in this mod. @@ -227,45 +227,45 @@ public class ModGroupEditor( => group switch { SingleModGroup s => SingleEditor.AddOption(s, option), - MultiModGroup m => MultiEditor.AddOption(m, option), - ImcModGroup i => ImcEditor.AddOption(i, option), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, }; public IModOption? AddOption(IModGroup group, string newName) => group switch { SingleModGroup s => SingleEditor.AddOption(s, newName), - MultiModGroup m => MultiEditor.AddOption(m, newName), - ImcModGroup i => ImcEditor.AddOption(i, newName), - _ => null, + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, }; public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), - GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), - _ => null, + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), + _ => null, }; public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) => type switch { GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), - _ => (null, -1, false), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), }; public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) => group switch { SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), - MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), - ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), - _ => (null, -1, false), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), }; public void MoveOption(IModOption option, int toIdx) diff --git a/Penumbra/UI/ModsTab/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs similarity index 83% rename from Penumbra/UI/ModsTab/AddGroupDrawer.cs rename to Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 79c1bf9d..06cb4154 100644 --- a/Penumbra/UI/ModsTab/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -12,25 +12,25 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.UI.Classes; -namespace Penumbra.UI.ModsTab; +namespace Penumbra.UI.ModsTab.Groups; public class AddGroupDrawer : IUiService { - private string _groupName = string.Empty; - private bool _groupNameValid = false; + private string _groupName = string.Empty; + private bool _groupNameValid = false; - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; + private readonly ModManager _modManager; public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) { _metaManager = metaManager; - _modManager = modManager; + _modManager = modManager; UpdateEntry(); } @@ -61,7 +61,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -74,7 +74,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -126,7 +126,7 @@ public class AddGroupDrawer : IUiService width, !_groupNameValid || _entryInvalid)) { _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -150,12 +150,12 @@ public class AddGroupDrawer : IUiService } catch (Exception) { - _defaultEntry = new ImcEntry(); + _defaultEntry = new ImcEntry(); _imcFileExists = false; - _entryExists = false; + _entryExists = false; } - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); _entryInvalid = !_imcManip.Validate(true); } } diff --git a/Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs new file mode 100644 index 00000000..d7114147 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/IModGroupEditDrawer.cs @@ -0,0 +1,6 @@ +namespace Penumbra.UI.ModsTab.Groups; + +public interface IModGroupEditDrawer +{ + public void Draw(); +} diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs new file mode 100644 index 00000000..2418c5cb --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -0,0 +1,138 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Object Type"u8); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text("Slot"u8); + ImUtf8.Text("Primary ID"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text("Secondary ID"); + ImUtf8.Text("Variant"u8); + + ImUtf8.TextFrameAligned("Material ID"u8); + ImUtf8.TextFrameAligned("Material Animation ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Sound ID"u8); + ImUtf8.TextFrameAligned("Can Be Disabled"u8); + ImUtf8.TextFrameAligned("Default Attributes"u8); + } + + ImGui.SameLine(); + + var attributeCache = new ImcAttributeCache(group); + + using (ImUtf8.Group()) + { + ImUtf8.Text(group.ObjectType.ToName()); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text(group.EquipSlot.ToName()); + ImUtf8.Text($"{group.PrimaryId.Id}"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text($"{group.SecondaryId.Id}"); + ImUtf8.Text($"{group.Variant.Id}"); + + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); + + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); + + var defaultDisabled = group.DefaultDisabled; + ImUtf8.SameLineInner(); + if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) + editor.ModManager.OptionEditor.ChangeModGroupDefaultOption(group, + group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); + + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + } + + + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + editor.OptionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + + DrawNewOption(attributeCache); + return; + + static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & 1 << i) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip($"{(char)('A' + i)}"); + if (i != 9) + ImUtf8.SameLineInner(); + } + } + } + + private void DrawNewOption(in ImcAttributeCache cache) + { + if (cache.LowestUnsetMask == 0) + return; + + var name = editor.DrawNewOptionBase(group, group.Options.Count); + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs similarity index 85% rename from Penumbra/UI/ModsTab/ModGroupDrawer.cs rename to Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index e9b0b396..dec77430 100644 --- a/Penumbra/UI/ModsTab/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -11,7 +11,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; -namespace Penumbra.UI.ModsTab; +namespace Penumbra.UI.ModsTab.Groups; public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService { @@ -63,8 +63,8 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); var options = group.Options; using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) @@ -97,10 +97,10 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -110,8 +110,8 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle { for (var idx = 0; idx < group.Options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; + using var i = ImRaii.PushId(idx); + var option = options[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) SetModSetting(group, groupIdx, Setting.Single(idx)); @@ -130,9 +130,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImRaii.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -147,9 +147,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle { for (var idx = 0; idx < options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var enabled = setting.HasFlag(idx); + using var i = ImRaii.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref enabled)) SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); @@ -187,8 +187,8 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } else { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var buttonTextShow = $"Show {options.Count} Options"; var buttonTextHide = $"Hide {options.Count} Options"; var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) @@ -204,7 +204,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs new file mode 100644 index 00000000..e7d70922 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -0,0 +1,360 @@ +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.EndObjects; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab.Groups; + +public sealed class ModGroupEditDrawer( + ModManager modManager, + Configuration config, + FilenameService filenames, + DescriptionEditPopup descriptionPopup) : IUiService +{ + private static ReadOnlySpan DragDropLabel + => "##DragOption"u8; + + internal readonly ModManager ModManager = modManager; + internal readonly Queue ActionQueue = new(); + + internal Vector2 OptionIdxSelectable; + internal Vector2 AvailableWidth; + internal float PriorityWidth; + + internal string? NewOptionName; + private IModGroup? _newOptionGroup; + + private Vector2 _buttonSize; + private float _groupNameWidth; + private float _optionNameWidth; + private float _spacing; + private bool _deleteEnabled; + + private string? _currentGroupName; + private ModPriority? _currentGroupPriority; + private IModGroup? _currentGroupEdited; + private bool _isGroupNameValid = true; + + private IModGroup? _dragDropGroup; + private IModOption? _dragDropOption; + + public void Draw(Mod mod) + { + PrepareStyle(); + + using var id = ImUtf8.PushId("##GroupEdit"u8); + foreach (var (group, groupIdx) in mod.Groups.WithIndex()) + DrawGroup(group, groupIdx); + + while (ActionQueue.TryDequeue(out var action)) + action.Invoke(); + } + + private void DrawGroup(IModGroup group, int idx) + { + using var id = ImUtf8.PushId(idx); + using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); + DrawGroupNameRow(group, idx); + group.EditDrawer(this).Draw(); + } + + private void DrawGroupNameRow(IModGroup group, int idx) + { + DrawGroupName(group); + ImUtf8.SameLineInner(); + DrawGroupMoveButtons(group, idx); + ImUtf8.SameLineInner(); + DrawGroupOpenFile(group, idx); + ImUtf8.SameLineInner(); + DrawGroupDescription(group); + ImUtf8.SameLineInner(); + DrawGroupDelete(group); + ImUtf8.SameLineInner(); + DrawGroupPriority(group); + } + + private void DrawGroupName(IModGroup group) + { + var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; + ImGui.SetNextItemWidth(_groupNameWidth); + using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); + if (ImUtf8.InputText("##GroupName"u8, ref text)) + { + _currentGroupEdited = group; + _currentGroupName = text; + _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupName != null && _isGroupNameValid) + ModManager.OptionEditor.RenameModGroup(group, _currentGroupName); + _currentGroupName = null; + _currentGroupEdited = null; + _isGroupNameValid = true; + } + + var tt = _isGroupNameValid + ? "Change the Group name."u8 + : "Current name can not be used for this group."u8; + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); + } + + private void DrawGroupDelete(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option group."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + private void DrawGroupPriority(IModGroup group) + { + var priority = _currentGroupEdited == group + ? (_currentGroupPriority ?? group.Priority).Value + : group.Priority.Value; + ImGui.SetNextItemWidth(PriorityWidth); + if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) + { + _currentGroupEdited = group; + _currentGroupPriority = new ModPriority(priority); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupPriority.HasValue) + ModManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); + _currentGroupEdited = null; + _currentGroupPriority = null; + } + + ImGuiUtil.HoverTooltip("Group Priority"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawGroupDescription(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) + descriptionPopup.Open(group); + } + + private void DrawGroupMoveButtons(IModGroup group, int idx) + { + var isFirst = idx == 0; + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1)); + + if (isFirst) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); + else + ImUtf8.HoverTooltip($"Move this group up to group {idx}."); + + + ImUtf8.SameLineInner(); + var isLast = idx == group.Mod.Groups.Count - 1; + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1)); + + if (isLast) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); + else + ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); + } + + private void DrawGroupOpenFile(IModGroup group, int idx) + { + var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); + var fileExists = File.Exists(fileName); + if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); + } + + if (fileExists) + ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) + { + ImGui.AlignTextToFramePadding(); + ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: OptionIdxSelectable); + Target(group, optionIdx); + Source(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; + if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) + ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); + ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); + if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) + ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDescription(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) + descriptionPopup.Open(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionPriority(MultiSubMod option) + { + var priority = option.Priority.Value; + ImGui.SetNextItemWidth(PriorityWidth); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); + ImUtf8.HoverTooltip("Option priority inside the mod."u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionName(IModOption option) + { + var name = option.Name; + ImGui.SetNextItemWidth(_optionNameWidth); + if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) + ModManager.OptionEditor.RenameOption(option, name); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDelete(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal string DrawNewOptionBase(IModGroup group, int count) + { + ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable); + Target(group, count); + + ImUtf8.SameLineInner(); + ImUtf8.IconDummy(); + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(_optionNameWidth); + var newName = _newOptionGroup == group + ? NewOptionName ?? string.Empty + : string.Empty; + if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) + { + NewOptionName = newName; + _newOptionGroup = group; + } + + ImUtf8.SameLineInner(); + return newName; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Source(IModOption option) + { + if (option.Group is not ITexToolsGroup) + return; + + using var source = ImUtf8.DragDropSource(); + if (!source) + return; + + if (!DragDropSource.SetPayload(DragDropLabel)) + { + _dragDropGroup = option.Group; + _dragDropOption = option; + } + + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + } + + private void Target(IModGroup group, int optionIdx) + { + if (group is not ITexToolsGroup) + return; + + if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + return; + + if (_dragDropGroup != null && _dragDropOption != null) + { + if (_dragDropGroup == group) + { + var sourceOption = _dragDropOption; + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceOption = _dragDropOption; + ActionQueue.Enqueue(() => + { + ModManager.OptionEditor.DeleteOption(sourceOption); + if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + ModManager.OptionEditor.MoveOption(newOption, optionIdx); + }); + } + } + + _dragDropGroup = null; + _dragDropOption = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepareStyle() + { + var totalWidth = 400f * ImUtf8.GlobalScale; + _buttonSize = new Vector2(ImUtf8.FrameHeight); + PriorityWidth = 50 * ImUtf8.GlobalScale; + AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0); + _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + OptionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); + _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing; + _deleteEnabled = config.DeleteModModifier.IsActive(); + } +} diff --git a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs new file mode 100644 index 00000000..e6701a03 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs @@ -0,0 +1,63 @@ +using Dalamud.Interface; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionPriority(option); + } + + DrawNewOption(); + DrawConvertButton(); + } + + private void DrawConvertButton() + { + var g = group; + var e = editor.ModManager.OptionEditor.MultiEditor; + if (ImUtf8.Button("Convert to Single Group"u8, editor.AvailableWidth)) + editor.ActionQueue.Enqueue(() => e.ChangeToSingle(g)); + } + + private void DrawNewOption() + { + var count = group.Options.Count; + if (count >= IModGroup.MaxMultiOptions) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.MultiEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs new file mode 100644 index 00000000..75fbc63a --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -0,0 +1,68 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); + } + + DrawNewOption(); + DrawConvertButton(); + } + + private void DrawConvertButton() + { + var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; + var g = group; + var e = editor.ModManager.OptionEditor.SingleEditor; + if (ImUtf8.ButtonEx("Convert to Multi Group", editor.AvailableWidth, !convertible)) + editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g)); + if (!convertible) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Can not convert to multi group since maximum number of options is exceeded."u8); + } + + private void DrawNewOption() + { + var count = group.Options.Count; + if (count >= int.MaxValue) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs b/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs new file mode 100644 index 00000000..1f2273b5 --- /dev/null +++ b/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs @@ -0,0 +1,105 @@ +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public static class MetaManipulationDrawer +{ + public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + { + var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, + manip.Variant.Id, equipSlot, manip.Entry); + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + manip.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (manip.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, + manip.Entry); + return ret; + } + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs deleted file mode 100644 index 4ef1577f..00000000 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ /dev/null @@ -1,687 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Services; -using OtterGui.Text; -using OtterGui.Text.EndObjects; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.Mods.Groups; -using Penumbra.Mods.Manager; -using Penumbra.Mods.Manager.OptionEditor; -using Penumbra.Mods.Settings; -using Penumbra.Mods.SubMods; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public static class MetaManipulationDrawer -{ - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) - { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (manip.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); - return ret; - } - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } -} - -public sealed class ModGroupEditDrawer( - ModManager modManager, - Configuration config, - FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService -{ - private static ReadOnlySpan DragDropLabel - => "##DragOption"u8; - - private Vector2 _buttonSize; - private Vector2 _availableWidth; - private float _priorityWidth; - private float _groupNameWidth; - private float _optionNameWidth; - private float _spacing; - private Vector2 _optionIdxSelectable; - private bool _deleteEnabled; - - private string? _currentGroupName; - private ModPriority? _currentGroupPriority; - private IModGroup? _currentGroupEdited; - private bool _isGroupNameValid = true; - - private string? _newOptionName; - private IModGroup? _newOptionGroup; - private readonly Queue _actionQueue = new(); - - private IModGroup? _dragDropGroup; - private IModOption? _dragDropOption; - - public void Draw(Mod mod) - { - PrepareStyle(); - - using var id = ImUtf8.PushId("##GroupEdit"u8); - foreach (var (group, groupIdx) in mod.Groups.WithIndex()) - DrawGroup(group, groupIdx); - - while (_actionQueue.TryDequeue(out var action)) - action.Invoke(); - } - - private void DrawGroup(IModGroup group, int idx) - { - using var id = ImUtf8.PushId(idx); - using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); - DrawGroupNameRow(group, idx); - switch (group) - { - case SingleModGroup s: - DrawSingleGroup(s); - break; - case MultiModGroup m: - DrawMultiGroup(m); - break; - case ImcModGroup i: - DrawImcGroup(i); - break; - } - } - - private void DrawGroupNameRow(IModGroup group, int idx) - { - DrawGroupName(group); - ImUtf8.SameLineInner(); - DrawGroupMoveButtons(group, idx); - ImUtf8.SameLineInner(); - DrawGroupOpenFile(group, idx); - ImUtf8.SameLineInner(); - DrawGroupDescription(group); - ImUtf8.SameLineInner(); - DrawGroupDelete(group); - ImUtf8.SameLineInner(); - DrawGroupPriority(group); - } - - private void DrawGroupName(IModGroup group) - { - var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; - ImGui.SetNextItemWidth(_groupNameWidth); - using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); - if (ImUtf8.InputText("##GroupName"u8, ref text)) - { - _currentGroupEdited = group; - _currentGroupName = text; - _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); - } - - if (ImGui.IsItemDeactivated()) - { - if (_currentGroupName != null && _isGroupNameValid) - modManager.OptionEditor.RenameModGroup(group, _currentGroupName); - _currentGroupName = null; - _currentGroupEdited = null; - _isGroupNameValid = true; - } - - var tt = _isGroupNameValid - ? "Change the Group name."u8 - : "Current name can not be used for this group."u8; - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); - } - - private void DrawGroupDelete(IModGroup group) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) - _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteModGroup(group)); - - if (_deleteEnabled) - ImUtf8.HoverTooltip("Delete this option group."u8); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); - } - - private void DrawGroupPriority(IModGroup group) - { - var priority = _currentGroupEdited == group - ? (_currentGroupPriority ?? group.Priority).Value - : group.Priority.Value; - ImGui.SetNextItemWidth(_priorityWidth); - if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) - { - _currentGroupEdited = group; - _currentGroupPriority = new ModPriority(priority); - } - - if (ImGui.IsItemDeactivated()) - { - if (_currentGroupPriority.HasValue) - modManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); - _currentGroupEdited = null; - _currentGroupPriority = null; - } - - ImGuiUtil.HoverTooltip("Group Priority"); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawGroupDescription(IModGroup group) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) - descriptionPopup.Open(group); - } - - private void DrawGroupMoveButtons(IModGroup group, int idx) - { - var isFirst = idx == 0; - if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) - _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx - 1)); - - if (isFirst) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); - else - ImUtf8.HoverTooltip($"Move this group up to group {idx}."); - - - ImUtf8.SameLineInner(); - var isLast = idx == group.Mod.Groups.Count - 1; - if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) - _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx + 1)); - - if (isLast) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); - else - ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); - } - - private void DrawGroupOpenFile(IModGroup group, int idx) - { - var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); - var fileExists = File.Exists(fileName); - if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) - try - { - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - } - catch (Exception e) - { - Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); - } - - if (fileExists) - ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); - } - - private void DrawSingleGroup(SingleModGroup group) - { - foreach (var (option, optionIdx) in group.OptionData.WithIndex()) - { - using var id = ImRaii.PushId(optionIdx); - DrawOptionPosition(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionDefaultSingleBehaviour(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionName(option); - - ImUtf8.SameLineInner(); - DrawOptionDescription(option); - - ImUtf8.SameLineInner(); - DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(_priorityWidth, 0)); - } - - DrawNewOption(group); - var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; - if (ImUtf8.ButtonEx("Convert to Multi Group", _availableWidth, !convertible)) - _actionQueue.Enqueue(() => modManager.OptionEditor.SingleEditor.ChangeToMulti(group)); - if (!convertible) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - "Can not convert to multi group since maximum number of options is exceeded."u8); - } - - private void DrawMultiGroup(MultiModGroup group) - { - foreach (var (option, optionIdx) in group.OptionData.WithIndex()) - { - using var id = ImRaii.PushId(optionIdx); - DrawOptionPosition(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionDefaultMultiBehaviour(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionName(option); - - ImUtf8.SameLineInner(); - DrawOptionDescription(option); - - ImUtf8.SameLineInner(); - DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - DrawOptionPriority(option); - } - - DrawNewOption(group); - if (ImUtf8.Button("Convert to Single Group"u8, _availableWidth)) - _actionQueue.Enqueue(() => modManager.OptionEditor.MultiEditor.ChangeToSingle(group)); - } - - private void DrawImcGroup(ImcModGroup group) - { - using (ImUtf8.Group()) - { - ImUtf8.Text("Object Type"u8); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text("Slot"u8); - ImUtf8.Text("Primary ID"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text("Secondary ID"); - ImUtf8.Text("Variant"u8); - - ImUtf8.TextFrameAligned("Material ID"u8); - ImUtf8.TextFrameAligned("Material Animation ID"u8); - ImUtf8.TextFrameAligned("Decal ID"u8); - ImUtf8.TextFrameAligned("VFX ID"u8); - ImUtf8.TextFrameAligned("Sound ID"u8); - ImUtf8.TextFrameAligned("Can Be Disabled"u8); - ImUtf8.TextFrameAligned("Default Attributes"u8); - } - - ImGui.SameLine(); - - var attributeCache = new ImcAttributeCache(group); - - using (ImUtf8.Group()) - { - ImUtf8.Text(group.ObjectType.ToName()); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text(group.EquipSlot.ToName()); - ImUtf8.Text($"{group.PrimaryId.Id}"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text($"{group.SecondaryId.Id}"); - ImUtf8.Text($"{group.Variant.Id}"); - - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); - - var canBeDisabled = group.CanBeDisabled; - if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) - modManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); - - var defaultDisabled = group.DefaultDisabled; - ImUtf8.SameLineInner(); - if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) - modManager.OptionEditor.ChangeModGroupDefaultOption(group, - group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); - - DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); - } - - - foreach (var (option, optionIdx) in group.OptionData.WithIndex()) - { - using var id = ImRaii.PushId(optionIdx); - DrawOptionPosition(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionDefaultMultiBehaviour(group, option, optionIdx); - - ImUtf8.SameLineInner(); - DrawOptionName(option); - - ImUtf8.SameLineInner(); - DrawOptionDescription(option); - - ImUtf8.SameLineInner(); - DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(_priorityWidth, 0)); - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + _optionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); - DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); - } - - DrawNewOption(group, attributeCache); - return; - - static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) - { - for (var i = 0; i < ImcEntry.NumAttributes; ++i) - { - using var id = ImRaii.PushId(i); - var value = (mask & (1 << i)) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) - { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) - { - if (data is ImcModGroup g) - editor.ChangeDefaultAttribute(g, cache, i, value); - else - editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); - } - } - - ImUtf8.HoverTooltip($"{(char)('A' + i)}"); - if (i != 9) - ImUtf8.SameLineInner(); - } - } - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) - { - ImGui.AlignTextToFramePadding(); - ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: _optionIdxSelectable); - Target(group, optionIdx); - Source(option); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) - { - var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; - if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) - modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); - ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) - { - var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); - if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) - modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); - ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDescription(IModOption option) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) - descriptionPopup.Open(option); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionPriority(MultiSubMod option) - { - var priority = option.Priority.Value; - ImGui.SetNextItemWidth(_priorityWidth); - if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) - modManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); - ImUtf8.HoverTooltip("Option priority inside the mod."u8); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionName(IModOption option) - { - var name = option.Name; - ImGui.SetNextItemWidth(_optionNameWidth); - if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) - modManager.OptionEditor.RenameOption(option, name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawOptionDelete(IModOption option) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) - _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteOption(option)); - - if (_deleteEnabled) - ImUtf8.HoverTooltip("Delete this option."u8); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); - } - - private void DrawNewOption(SingleModGroup group) - { - var count = group.Options.Count; - if (count >= int.MaxValue) - return; - - var name = DrawNewOptionBase(group, count); - - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) - { - modManager.OptionEditor.SingleEditor.AddOption(group, name); - _newOptionName = null; - } - } - - private void DrawNewOption(MultiModGroup group) - { - var count = group.Options.Count; - if (count >= IModGroup.MaxMultiOptions) - return; - - var name = DrawNewOptionBase(group, count); - - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) - { - modManager.OptionEditor.MultiEditor.AddOption(group, name); - _newOptionName = null; - } - } - - private void DrawNewOption(ImcModGroup group, in ImcAttributeCache cache) - { - if (cache.LowestUnsetMask == 0) - return; - - var name = DrawNewOptionBase(group, group.Options.Count); - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) - { - modManager.OptionEditor.ImcEditor.AddOption(group, cache, name); - _newOptionName = null; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string DrawNewOptionBase(IModGroup group, int count) - { - ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); - Target(group, count); - - ImUtf8.SameLineInner(); - ImUtf8.IconDummy(); - - ImUtf8.SameLineInner(); - ImGui.SetNextItemWidth(_optionNameWidth); - var newName = _newOptionGroup == group - ? _newOptionName ?? string.Empty - : string.Empty; - if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) - { - _newOptionName = newName; - _newOptionGroup = group; - } - - ImUtf8.SameLineInner(); - return newName; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Source(IModOption option) - { - if (option.Group is not ITexToolsGroup) - return; - - using var source = ImUtf8.DragDropSource(); - if (!source) - return; - - if (!DragDropSource.SetPayload(DragDropLabel)) - { - _dragDropGroup = option.Group; - _dragDropOption = option; - } - - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); - } - - private void Target(IModGroup group, int optionIdx) - { - if (group is not ITexToolsGroup) - return; - - if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) - return; - - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) - return; - - if (_dragDropGroup != null && _dragDropOption != null) - { - if (_dragDropGroup == group) - { - var sourceOption = _dragDropOption; - _actionQueue.Enqueue(() => modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceOption = _dragDropOption; - _actionQueue.Enqueue(() => - { - modManager.OptionEditor.DeleteOption(sourceOption); - if (modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) - modManager.OptionEditor.MoveOption(newOption, optionIdx); - }); - } - } - - _dragDropGroup = null; - _dragDropOption = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PrepareStyle() - { - var totalWidth = 400f * ImUtf8.GlobalScale; - _buttonSize = new Vector2(ImUtf8.FrameHeight); - _priorityWidth = 50 * ImUtf8.GlobalScale; - _availableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + _priorityWidth, 0); - _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); - _spacing = ImGui.GetStyle().ItemInnerSpacing.X; - _optionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); - _optionNameWidth = totalWidth - _optionIdxSelectable.X - _buttonSize.X - 2 * _spacing; - _deleteEnabled = config.DeleteModModifier.IsActive(); - } -} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index b7951c49..125f539e 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -13,6 +13,7 @@ using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 107a2d04..7e3b8a95 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -9,6 +9,7 @@ using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Mods.Settings; +using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; From c06d5b08715924aba67910f7e0fe718b4f83b274 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 16:57:16 +0200 Subject: [PATCH 089/865] Update for new gamedata. --- Penumbra.GameData | 2 +- Penumbra/Import/TexToolsMeta.Deserialization.cs | 4 +--- Penumbra/Meta/Files/CmpFile.cs | 6 +++--- Penumbra/Meta/Files/EqpGmpFile.cs | 6 +++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 5fa4d0e7..e8220a0a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 5fa4d0e7972423b73f8cf569bb2bfbeddd825c8a +Subproject commit e8220a0a74e9480330e98ed7ca462353434b9649 diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 325c9143..f062ae25 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -57,9 +57,7 @@ public partial class TexToolsMeta if (data == null) return; - using var reader = new BinaryReader(new MemoryStream(data)); - var value = (GmpEntry)reader.ReadUInt32(); - value.UnknownTotal = reader.ReadByte(); + var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); if (_keepDefault || value != def) MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId)); diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 8a6040ec..b265a5e8 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -19,8 +19,8 @@ public sealed unsafe class CmpFile : MetaBaseFile public float this[SubRace subRace, RspAttribute attribute] { - get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4); - set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4) = value; + get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; } public override void Reset() @@ -42,7 +42,7 @@ public sealed unsafe class CmpFile : MetaBaseFile public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; - return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4); + return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); } private static int ToRspIndex(SubRace subRace) diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 97f57703..70067c2b 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -157,12 +157,12 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable public GmpEntry this[PrimaryId idx] { - get => (GmpEntry)GetInternal(idx); - set => SetInternal(idx, (ulong)value); + get => new() { Value = GetInternal(idx) }; + set => SetInternal(idx, value.Value); } public static GmpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx) - => (GmpEntry)GetDefaultInternal(manager, InternalIndex, primaryIdx, (ulong)GmpEntry.Default); + => new() { Value = GetDefaultInternal(manager, InternalIndex, primaryIdx, GmpEntry.Default.Value) }; public void Reset(IEnumerable entries) { From dfdd5167a84c5a9c1aeb5280eb3fb91d895a613b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 17:24:42 +0200 Subject: [PATCH 090/865] Remove auto descriptions from newly generated option groups. --- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index d2c41f34..cf228889 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -22,7 +22,7 @@ public class ImcModGroup(Mod mod) : IModGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A single IMC manipulation."; + public string Description { get; set; } = string.Empty; public GroupType Type => GroupType.Imc; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7fc9acb3..38c0ef15 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -25,7 +25,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; + public string Description { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 4eec0746..49190e34 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -23,7 +23,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; + public string Description { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } From 7df9ddcb995b1f4b86abcfb827a63fb5488e6c1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 17:25:27 +0200 Subject: [PATCH 091/865] Re-Add button to open default mod json. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 125f539e..468e97b9 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -35,8 +35,8 @@ public class ModPanelEditTab( { private readonly TagButtons _modTags = new(); - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; public ReadOnlySpan Label => "Edit Mod"u8; @@ -193,6 +193,7 @@ public class ModPanelEditTab( if (ImGui.Button("Edit Description", reducedSize)) descriptionPopup.Open(_mod); + ImGui.SameLine(); var fileExists = File.Exists(filenames.ModMetaPath(_mod)); var tt = fileExists @@ -201,8 +202,22 @@ public class ModPanelEditTab( if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); + + DrawOpenDefaultMod(); } + private void DrawOpenDefaultMod() + { + var file = filenames.OptionGroupFile(_mod, -1, false); + var fileExists = File.Exists(file); + var tt = fileExists + ? "Open the default mod data file in the text editor of your choice." + : "The default mod data file does not exist."; + if (ImGuiUtil.DrawDisabledButton("Open Default Data", UiHelpers.InputTextWidth, tt, !fileExists)) + Process.Start(new ProcessStartInfo(file) { UseShellExecute = true }); + } + + /// A text input for the new directory name and a button to apply the move. private static class MoveDirectory { From fca1bf9d946dc505c1834ccef6feb206ee3039f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 22:30:42 +0200 Subject: [PATCH 092/865] Add ImcIdentifier. --- .../Meta/Manipulations/IMetaIdentifier.cs | 16 ++ Penumbra/Meta/Manipulations/Imc.cs | 187 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 Penumbra/Meta/Manipulations/IMetaIdentifier.cs create mode 100644 Penumbra/Meta/Manipulations/Imc.cs diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs new file mode 100644 index 00000000..4ad6bd3d --- /dev/null +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public interface IMetaIdentifier +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + + public MetaIndex FileIndex(); + + public bool Validate(); + + public JObject AddToJson(JObject jObj); +} diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs new file mode 100644 index 00000000..f0101be2 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -0,0 +1,187 @@ +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.String.Classes; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct ImcIdentifier( + PrimaryId PrimaryId, + Variant Variant, + ObjectType ObjectType, + SecondaryId SecondaryId, + EquipSlot EquipSlot, + BodySlot BodySlot) : IMetaIdentifier, IComparable +{ + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, ushort variant) + : this(primaryId, (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue), + slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, + variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown) + { } + + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, Variant variant) + : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) + { } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + var path = ObjectType switch + { + ObjectType.Equipment or ObjectType.Accessory => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, + Variant, + "a"), + ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), + ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, + "a"), + ObjectType.Monster => GamePaths.Monster.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), + _ => string.Empty, + }; + if (path.Length == 0) + return; + + identifier.Identify(changedItems, path); + } + + public Utf8GamePath GamePath() + { + return ObjectType switch + { + ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, + ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, + ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), out var p) + ? p + : Utf8GamePath.Empty, + ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), out var p) + ? p + : Utf8GamePath.Empty, + ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), out var p) + ? p + : Utf8GamePath.Empty, + _ => throw new NotImplementedException(), + }; + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public override string ToString() + => ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}" + : $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}"; + + public bool Validate() + { + switch (ObjectType) + { + case ObjectType.Accessory: + case ObjectType.Equipment: + if (BodySlot is not BodySlot.Unknown) + return false; + if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) + return false; + if (SecondaryId != 0) + return false; + + break; + case ObjectType.DemiHuman: + if (BodySlot is not BodySlot.Unknown) + return false; + if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) + return false; + + break; + default: + if (!Enum.IsDefined(BodySlot)) + return false; + if (EquipSlot is not EquipSlot.Unknown) + return false; + if (!Enum.IsDefined(ObjectType)) + return false; + + break; + } + + return true; + } + + public int CompareTo(ImcIdentifier other) + { + var o = ObjectType.CompareTo(other.ObjectType); + if (o != 0) + return o; + + var i = PrimaryId.Id.CompareTo(other.PrimaryId.Id); + if (i != 0) + return i; + + if (ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + var e = EquipSlot.CompareTo(other.EquipSlot); + return e != 0 ? e : Variant.Id.CompareTo(other.Variant.Id); + } + + if (ObjectType is ObjectType.DemiHuman) + { + var e = EquipSlot.CompareTo(other.EquipSlot); + if (e != 0) + return e; + } + + var s = SecondaryId.Id.CompareTo(other.SecondaryId.Id); + if (s != 0) + return s; + + var b = BodySlot.CompareTo(other.BodySlot); + return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); + } + + public static ImcIdentifier? FromJson(JObject jObj) + { + var objectType = jObj["PrimaryId"]?.ToObject() ?? ObjectType.Unknown; + var primaryId = new PrimaryId(jObj["PrimaryId"]?.ToObject() ?? 0); + var variant = jObj["Variant"]?.ToObject() ?? 0; + if (variant > byte.MaxValue) + return null; + + ImcIdentifier ret; + switch (objectType) + { + case ObjectType.Equipment: + case ObjectType.Accessory: + { + var slot = jObj["EquipSlot"]?.ToObject() ?? EquipSlot.Unknown; + ret = new ImcIdentifier(slot, primaryId, variant); + break; + } + case ObjectType.DemiHuman: + { + var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown); + break; + } + + case ObjectType.Monster: + case ObjectType.Weapon: + { + var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); + ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, EquipSlot.Unknown, BodySlot.Body); + break; + } + default: return null; + } + + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} From 992cdff58d3a82c64dc166147de7de4a10be87f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 22:31:39 +0200 Subject: [PATCH 093/865] Improve some IMC things. --- OtterGui | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 14 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 92 +++----- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 18 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 221 ++++++++++++++++++ Penumbra/UI/ModsTab/MetaManipulationDrawer.cs | 105 --------- 6 files changed, 267 insertions(+), 185 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ImcManipulationDrawer.cs delete mode 100644 Penumbra/UI/ModsTab/MetaManipulationDrawer.cs diff --git a/OtterGui b/OtterGui index 462acb87..1d936516 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 462acb87099650019996e4306d18cc70f76ca576 +Subproject commit 1d9365164655a7cb38172e1311e15e19b1def6db diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index f0101be2..9b123df1 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -134,7 +135,7 @@ public readonly record struct ImcIdentifier( var b = BodySlot.CompareTo(other.BodySlot); return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); - } + } public static ImcIdentifier? FromJson(JObject jObj) { @@ -177,11 +178,12 @@ public readonly record struct ImcIdentifier( public JObject AddToJson(JObject jObj) { - var (gender, race) = GenderRace.Split(); - jObj["Gender"] = gender.ToString(); - jObj["Race"] = race.ToString(); - jObj["SetId"] = SetId.Id.ToString(); - jObj["Slot"] = Slot.ToString(); + jObj["ObjectType"] = ObjectType.ToString(); + jObj["PrimaryId"] = PrimaryId.Id; + jObj["PrimaryId"] = SecondaryId.Id; + jObj["Variant"] = Variant.Id; + jObj["EquipSlot"] = EquipSlot.ToString(); + jObj["BodySlot"] = BodySlot.ToString(); return jObj; } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 55125375..99889360 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -352,26 +352,26 @@ public partial class ModEditWindow // Identifier ImGui.TableNextColumn(); - var change = MetaManipulationDrawer.DrawObjectType(ref _new); + var change = ImcManipulationDrawer.DrawObjectType(ref _new); ImGui.TableNextColumn(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _new); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _new); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= MetaManipulationDrawer.DrawSlot(ref _new); + change |= ImcManipulationDrawer.DrawSlot(ref _new); else - change |= MetaManipulationDrawer.DrawSecondaryId(ref _new); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _new); ImGui.TableNextColumn(); - change |= MetaManipulationDrawer.DrawVariant(ref _new); + change |= ImcManipulationDrawer.DrawVariant(ref _new); ImGui.TableNextColumn(); if (_new.ObjectType is ObjectType.DemiHuman) - change |= MetaManipulationDrawer.DrawSlot(ref _new, 70); + change |= ImcManipulationDrawer.DrawSlot(ref _new, 70); else ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); @@ -379,32 +379,20 @@ public partial class ModEditWindow _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); // Values using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput("##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _, - 1, byte.MaxValue, 0f); - ImGui.SameLine(); - IntDragInput("##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId, - defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f); - ImGui.TableNextColumn(); - IntDragInput("##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0, - byte.MaxValue, 0f); - ImGui.SameLine(); - IntDragInput("##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue, - 0f); - ImGui.SameLine(); - IntDragInput("##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111, - 0f); - ImGui.TableNextColumn(); - for (var i = 0; i < 10; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - Checkmark("##attribute", $"{(char)('A' + i)}", (defaultEntry.Value.AttributeMask & flag) != 0, - (defaultEntry.Value.AttributeMask & flag) != 0, out _); - ImGui.SameLine(); - } - ImGui.NewLine(); + var entry = defaultEntry.Value; + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawMaterialId(entry, ref entry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawMaterialAnimationId(entry, ref entry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawDecalId(entry, ref entry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawVfxId(entry, ref entry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawSoundId(entry, ref entry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawAttributes(entry, ref entry); } public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) @@ -452,46 +440,22 @@ public partial class ModEditWindow new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry(); - if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, - defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId })); + var newEntry = meta.Entry; + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); ImGui.SameLine(); - if (IntDragInput("##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth, - meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialAnimationId = (byte)materialAnimId })); - + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); - if (IntDragInput("##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId, - defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { DecalId = (byte)decalId })); - + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); ImGui.SameLine(); - if (IntDragInput("##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId, - out var vfxId, 0, byte.MaxValue, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { VfxId = (byte)vfxId })); - + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); ImGui.SameLine(); - if (IntDragInput("##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId, - defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { SoundId = (byte)soundId })); - + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); - for (var i = 0; i < 10; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - if (Checkmark("##attribute", $"{(char)('A' + i)}", (meta.Entry.AttributeMask & flag) != 0, - (defaultEntry.AttributeMask & flag) != 0, out var val)) - { - var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag; - editor.MetaEditor.Change(meta.Copy(meta.Entry with { AttributeMask = (ushort)attributes })); - } + changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - ImGui.SameLine(); - } - - ImGui.NewLine(); + if (changes) + editor.MetaEditor.Change(meta.Copy(newEntry)); } } diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 06cb4154..2d80d3df 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -80,29 +80,29 @@ public class AddGroupDrawer : IUiService private void DrawImcInput(float width) { - var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, width); + var change = ImcManipulationDrawer.DrawObjectType(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcManip, width); if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); } else if (_imcManip.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); } else { - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, width); ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); } if (change) diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs new file mode 100644 index 00000000..5873119e --- /dev/null +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -0,0 +1,221 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.HelperObjects; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public static class ImcManipulationDrawer +{ + public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + { + var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, + manip.Variant.Id, equipSlot, manip.Entry); + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + manip.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, + manip.Entry); + return ret; + } + + public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (manip.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, + manip.Entry); + return ret; + } + + public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, + out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialId = newValue }; + return true; + } + + public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, + defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialAnimationId = newValue }; + return true; + } + + public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { DecalId = newValue }; + return true; + } + + public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, + byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { VfxId = newValue }; + return true; + } + + public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { SoundId = newValue }; + return true; + } + + public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + { + var changes = false; + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (entry.AttributeMask & flag) != 0; + var def = (defaultEntry.AttributeMask & flag) != 0; + if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) + { + var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); + entry = entry with { AttributeMask = newMask }; + changes = true; + } + + if (i < ImcEntry.NumAttributes - 1) + ImGui.SameLine(); + } + + return changes; + } + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } +} diff --git a/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs b/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs deleted file mode 100644 index 1f2273b5..00000000 --- a/Penumbra/UI/ModsTab/MetaManipulationDrawer.cs +++ /dev/null @@ -1,105 +0,0 @@ -using ImGuiNET; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.GameData.Enums; -using Penumbra.Meta.Manipulations; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public static class MetaManipulationDrawer -{ - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) - { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); - return ret; - } - - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (manip.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); - return ret; - } - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } -} From 125e5628ecbf2e42d75e8c499db39bdeef3ba668 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 May 2024 23:24:37 +0200 Subject: [PATCH 094/865] Fix fuckup. --- Penumbra/Mods/Groups/ModSaveGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 7efc76a6..05437e3d 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -72,7 +72,7 @@ public readonly struct ModSaveGroup : ISavable var serializer = new JsonSerializer { Formatting = Formatting.Indented }; j.WriteStartObject(); if (_groupIdx >= 0) - _group!.WriteJson(j, serializer); + _group!.WriteJson(j, serializer, _basePath); else SubMod.WriteModContainer(j, serializer, _defaultMod!, _basePath); j.WriteEndObject(); From 65627b5002c2b1ff9ed6b73cd5d1e7efb7ef2963 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 16:14:48 +0200 Subject: [PATCH 095/865] Fix a weird age-old bug apparently? --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index ca57c8b9..ae424b94 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -274,17 +274,17 @@ public class CollectionCacheManager : IDisposable return; } - type.HandlingInfo(out _, out var recomputeList, out var reload); + type.HandlingInfo(out _, out var recomputeList, out var justAdd); if (!recomputeList) return; foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) { - if (reload) - collection._cache!.ReloadMod(mod, true); - else + if (justAdd) collection._cache!.AddMod(mod, true); + else + collection._cache!.ReloadMod(mod, true); } } From 4743acf76786518307782677d6d38c48faeb0b95 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 16:15:04 +0200 Subject: [PATCH 096/865] Make IMC handling even better. --- Penumbra.GameData | 2 +- Penumbra/Meta/ImcChecker.cs | 36 +++ Penumbra/Meta/Manipulations/Imc.cs | 24 +- .../Meta/Manipulations/ImcManipulation.cs | 169 ++++-------- Penumbra/Meta/MetaFileManager.cs | 2 + Penumbra/Mods/Groups/ImcModGroup.cs | 251 ++++++++++-------- .../Manager/OptionEditor/ImcModGroupEditor.cs | 16 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/SubMods/ImcSubMod.cs | 12 +- Penumbra/Services/StaticServiceManager.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 65 ++--- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 68 ++--- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 161 +++++------ .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 32 ++- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 53 ++-- 15 files changed, 437 insertions(+), 459 deletions(-) create mode 100644 Penumbra/Meta/ImcChecker.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index e8220a0a..ec35e664 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e8220a0a74e9480330e98ed7ca462353434b9649 +Subproject commit ec35e66499eb388b4e7917e4fae4615218d33335 diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs new file mode 100644 index 00000000..14486e21 --- /dev/null +++ b/Penumbra/Meta/ImcChecker.cs @@ -0,0 +1,36 @@ +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta; + +public class ImcChecker(MetaFileManager metaFileManager) +{ + public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); + + private readonly Dictionary _cachedDefaultEntries = new(); + + public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) + { + if (_cachedDefaultEntries.TryGetValue(identifier, out var entry)) + return entry; + + try + { + var e = ImcFile.GetDefault(metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + entry = new CachedEntry(e, true, entryExists); + } + catch (Exception) + { + entry = new CachedEntry(default, false, false); + } + + if (storeCache) + _cachedDefaultEntries.Add(identifier, entry); + return entry; + } + + public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) + => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, + imcManip.EquipSlot, imcManip.BodySlot), storeCache); +} diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 9b123df1..fef86520 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -15,6 +15,8 @@ public readonly record struct ImcIdentifier( EquipSlot EquipSlot, BodySlot BodySlot) : IMetaIdentifier, IComparable { + public static readonly ImcIdentifier Default = new(EquipSlot.Body, 1, (Variant)1); + public ImcIdentifier(EquipSlot slot, PrimaryId primaryId, ushort variant) : this(primaryId, (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue), slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, @@ -25,6 +27,9 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } + public ImcManipulation ToManipulation(ImcEntry entry) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = ObjectType switch @@ -137,9 +142,12 @@ public readonly record struct ImcIdentifier( return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); } - public static ImcIdentifier? FromJson(JObject jObj) + public static ImcIdentifier? FromJson(JObject? jObj) { - var objectType = jObj["PrimaryId"]?.ToObject() ?? ObjectType.Unknown; + if (jObj == null) + return null; + + var objectType = jObj["ObjectType"]?.ToObject() ?? ObjectType.Unknown; var primaryId = new PrimaryId(jObj["PrimaryId"]?.ToObject() ?? 0); var variant = jObj["Variant"]?.ToObject() ?? 0; if (variant > byte.MaxValue) @@ -178,12 +186,12 @@ public readonly record struct ImcIdentifier( public JObject AddToJson(JObject jObj) { - jObj["ObjectType"] = ObjectType.ToString(); - jObj["PrimaryId"] = PrimaryId.Id; - jObj["PrimaryId"] = SecondaryId.Id; - jObj["Variant"] = Variant.Id; - jObj["EquipSlot"] = EquipSlot.ToString(); - jObj["BodySlot"] = BodySlot.ToString(); + jObj["ObjectType"] = ObjectType.ToString(); + jObj["PrimaryId"] = PrimaryId.Id; + jObj["SecondaryId"] = SecondaryId.Id; + jObj["Variant"] = Variant.Id; + jObj["EquipSlot"] = EquipSlot.ToString(); + jObj["BodySlot"] = BodySlot.ToString(); return jObj; } } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 45295990..945aab04 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -12,171 +12,96 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct ImcManipulation : IMetaManipulation { - public ImcEntry Entry { get; private init; } - public PrimaryId PrimaryId { get; private init; } - public PrimaryId SecondaryId { get; private init; } - public Variant Variant { get; private init; } + [JsonIgnore] + public ImcIdentifier Identifier { get; private init; } + + public ImcEntry Entry { get; private init; } + + + public PrimaryId PrimaryId + => Identifier.PrimaryId; + + public SecondaryId SecondaryId + => Identifier.SecondaryId; + + public Variant Variant + => Identifier.Variant; [JsonConverter(typeof(StringEnumConverter))] - public ObjectType ObjectType { get; private init; } + public ObjectType ObjectType + => Identifier.ObjectType; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot EquipSlot { get; private init; } + public EquipSlot EquipSlot + => Identifier.EquipSlot; [JsonConverter(typeof(StringEnumConverter))] - public BodySlot BodySlot { get; private init; } + public BodySlot BodySlot + => Identifier.BodySlot; public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) + : this(new ImcIdentifier(equipSlot, primaryId, variant), entry) + { } + + public ImcManipulation(ImcIdentifier identifier, ImcEntry entry) { - Entry = entry; - PrimaryId = primaryId; - Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - SecondaryId = 0; - ObjectType = equipSlot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment; - EquipSlot = equipSlot; - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; + Identifier = identifier; + Entry = entry; } + // Variants were initially ushorts but got shortened to bytes. // There are still some manipulations around that have values > 255 for variant, // so we change the unused value to something nonsensical in that case, just so they do not compare equal, // and clamp the variant to 255. [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, PrimaryId secondaryId, ushort variant, + internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant, EquipSlot equipSlot, ImcEntry entry) { - Entry = entry; - ObjectType = objectType; - PrimaryId = primaryId; - Variant = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - - if (objectType is ObjectType.Accessory or ObjectType.Equipment) + Entry = entry; + var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); + Identifier = objectType switch { - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; - SecondaryId = 0; - EquipSlot = equipSlot; - } - else if (objectType is ObjectType.DemiHuman) - { - BodySlot = variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown; - SecondaryId = secondaryId; - EquipSlot = equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot; - } - else - { - BodySlot = bodySlot; - SecondaryId = secondaryId; - EquipSlot = variant > byte.MaxValue ? EquipSlot.All : EquipSlot.Unknown; - } + ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, + variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, + equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, + bodySlot), + }; } public ImcManipulation Copy(ImcEntry entry) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId, Variant.Id, EquipSlot, entry); + => new(Identifier, entry); public override string ToString() - => ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? $"Imc - {PrimaryId} - {EquipSlot} - {Variant}" - : $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}"; + => Identifier.ToString(); public bool Equals(ImcManipulation other) - => PrimaryId == other.PrimaryId - && Variant == other.Variant - && SecondaryId == other.SecondaryId - && ObjectType == other.ObjectType - && EquipSlot == other.EquipSlot - && BodySlot == other.BodySlot; + => Identifier == other.Identifier; public override bool Equals(object? obj) => obj is ImcManipulation other && Equals(other); public override int GetHashCode() - => HashCode.Combine(PrimaryId, Variant, SecondaryId, (int)ObjectType, (int)EquipSlot, (int)BodySlot); + => Identifier.GetHashCode(); public int CompareTo(ImcManipulation other) - { - var o = ObjectType.CompareTo(other.ObjectType); - if (o != 0) - return o; - - var i = PrimaryId.Id.CompareTo(other.PrimaryId.Id); - if (i != 0) - return i; - - if (ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - var e = EquipSlot.CompareTo(other.EquipSlot); - return e != 0 ? e : Variant.Id.CompareTo(other.Variant.Id); - } - - if (ObjectType is ObjectType.DemiHuman) - { - var e = EquipSlot.CompareTo(other.EquipSlot); - if (e != 0) - return e; - } - - var s = SecondaryId.Id.CompareTo(other.SecondaryId.Id); - if (s != 0) - return s; - - var b = BodySlot.CompareTo(other.BodySlot); - return b != 0 ? b : Variant.Id.CompareTo(other.Variant.Id); - } + => Identifier.CompareTo(other.Identifier); public MetaIndex FileIndex() - => (MetaIndex)(-1); + => Identifier.FileIndex(); public Utf8GamePath GamePath() - { - return ObjectType switch - { - ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId), out var p) ? p : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), - }; - } + => Identifier.GamePath(); public bool Apply(ImcFile file) => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); public bool Validate(bool withMaterial) { - switch (ObjectType) - { - case ObjectType.Accessory: - case ObjectType.Equipment: - if (BodySlot is not BodySlot.Unknown) - return false; - if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) - return false; - if (SecondaryId != 0) - return false; - - break; - case ObjectType.DemiHuman: - if (BodySlot is not BodySlot.Unknown) - return false; - if (!EquipSlot.IsEquipment() && !EquipSlot.IsAccessory()) - return false; - - break; - default: - if (!Enum.IsDefined(BodySlot)) - return false; - if (EquipSlot is not EquipSlot.Unknown) - return false; - if (!Enum.IsDefined(ObjectType)) - return false; - - break; - } + if (!Identifier.Validate()) + return false; if (withMaterial && Entry.MaterialId == 0) return false; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index cd99396b..40fceb07 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -27,6 +27,7 @@ public unsafe class MetaFileManager internal readonly ValidityChecker ValidityChecker; internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; + internal readonly ImcChecker ImcChecker; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, @@ -40,6 +41,7 @@ public unsafe class MetaFileManager ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; + ImcChecker = new ImcChecker(this); interop.InitializeFromAttributes(this); } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index cf228889..e0d70aa6 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,25 +1,21 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; -using Penumbra.UI.ModsTab; using Penumbra.UI.ModsTab.Groups; -using Penumbra.Util; namespace Penumbra.Mods.Groups; public class ImcModGroup(Mod mod) : IModGroup { - public const int DisabledIndex = 60; - public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = string.Empty; @@ -33,33 +29,35 @@ public class ImcModGroup(Mod mod) : IModGroup public ModPriority Priority { get; set; } = ModPriority.Default; public Setting DefaultSettings { get; set; } = Setting.Zero; - public PrimaryId PrimaryId; - public SecondaryId SecondaryId; - public ObjectType ObjectType; - public BodySlot BodySlot; - public EquipSlot EquipSlot; - public Variant Variant; - - public ImcEntry DefaultEntry; + public ImcIdentifier Identifier; + public ImcEntry DefaultEntry; public FullPath? FindBestMatch(Utf8GamePath gamePath) => null; - private bool _canBeDisabled = false; + private bool _canBeDisabled; public bool CanBeDisabled { - get => _canBeDisabled; + get => OptionData.Any(m => m.IsDisableSubMod); set { _canBeDisabled = value; if (!value) + { + OptionData.RemoveAll(m => m.IsDisableSubMod); DefaultSettings = FixSetting(DefaultSettings); + } + else + { + if (!OptionData.Any(m => m.IsDisableSubMod)) + OptionData.Add(ImcSubMod.DisableSubMod(this)); + } } } public bool DefaultDisabled - => _canBeDisabled && DefaultSettings.HasFlag(DisabledIndex); + => IsDisabled(DefaultSettings); public IModOption? AddOption(string name, string description = "") { @@ -86,7 +84,7 @@ public class ImcModGroup(Mod mod) : IModGroup => []; public bool IsOption - => CanBeDisabled || OptionData.Count > 0; + => OptionData.Count > 0; public int GetIndex() => ModGroup.GetIndex(this); @@ -94,6 +92,128 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); + public ImcManipulation GetManip(ushort mask) + => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, Identifier.Variant.Id, + Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); + + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + if (IsDisabled(setting)) + return; + + var mask = GetCurrentMask(setting); + var imc = GetManip(mask); + manipulations.Add(imc); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => Identifier.AddChangedItems(identifier, changedItems); + + public Setting FixSetting(Setting setting) + => new(setting.Value & ((1ul << OptionData.Count) - 1)); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + var jObj = Identifier.AddToJson(new JObject()); + jWriter.WritePropertyName(nameof(Identifier)); + jObj.WriteTo(jWriter); + jWriter.WritePropertyName(nameof(DefaultEntry)); + serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + if (option.IsDisableSubMod) + { + jWriter.WritePropertyName(nameof(option.IsDisableSubMod)); + jWriter.WriteValue(true); + } + else + { + jWriter.WritePropertyName(nameof(option.AttributeMask)); + jWriter.WriteValue(option.AttributeMask); + } + + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => (0, 0, 1); + + public static ImcModGroup? Load(Mod mod, JObject json) + { + var options = json["Options"]; + var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); + var ret = new ImcModGroup(mod) + { + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + }; + if (ret.Name.Length == 0) + return null; + + if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0) + { + Penumbra.Messager.NotificationMessage($"Could not add IMC group {ret.Name} because the associated IMC Entry is invalid.", + NotificationType.Warning); + return null; + } + + var rollingMask = ret.DefaultEntry.AttributeMask; + if (options != null) + foreach (var child in options.Children()) + { + var subMod = new ImcSubMod(ret, child); + + if (subMod.IsDisableSubMod) + ret._canBeDisabled = true; + + if (subMod.IsDisableSubMod && ret.OptionData.FirstOrDefault(m => m.IsDisableSubMod) is { } disable) + { + Penumbra.Messager.NotificationMessage( + $"Could not add IMC option {subMod.Name} to {ret.Name} because it already contains {disable.Name} as disable option.", + NotificationType.Warning); + } + else if ((subMod.AttributeMask & rollingMask) != 0) + { + Penumbra.Messager.NotificationMessage( + $"Could not add IMC option {subMod.Name} to {ret.Name} because it contains attributes already in use.", + NotificationType.Warning); + } + else + { + rollingMask |= subMod.AttributeMask; + ret.OptionData.Add(subMod); + } + } + + ret.Identifier = identifier.Value; + ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero; + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + return ret; + } + + private bool IsDisabled(Setting setting) + { + if (!CanBeDisabled) + return false; + + var idx = OptionData.IndexOf(m => m.IsDisableSubMod); + if (idx >= 0) + return setting.HasFlag(idx); + + Penumbra.Log.Warning($"A IMC Group should be able to be disabled, but does not contain a disable option."); + return false; + } + private ushort GetCurrentMask(Setting setting) { var mask = DefaultEntry.AttributeMask; @@ -108,101 +228,4 @@ public class ImcModGroup(Mod mod) : IModGroup return mask; } - - private ushort GetFullMask() - => GetCurrentMask(Setting.AllBits(63)); - - public ImcManipulation GetManip(ushort mask) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, - DefaultEntry with { AttributeMask = mask }); - - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - { - if (CanBeDisabled && setting.HasFlag(DisabledIndex)) - return; - - var mask = GetCurrentMask(setting); - var imc = GetManip(mask); - manipulations.Add(imc); - } - - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.MetaChangedItems(changedItems, GetManip(0)); - - public Setting FixSetting(Setting setting) - => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); - - public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) - { - ModSaveGroup.WriteJsonBase(jWriter, this); - jWriter.WritePropertyName(nameof(ObjectType)); - jWriter.WriteValue(ObjectType.ToString()); - jWriter.WritePropertyName(nameof(BodySlot)); - jWriter.WriteValue(BodySlot.ToString()); - jWriter.WritePropertyName(nameof(EquipSlot)); - jWriter.WriteValue(EquipSlot.ToString()); - jWriter.WritePropertyName(nameof(PrimaryId)); - jWriter.WriteValue(PrimaryId.Id); - jWriter.WritePropertyName(nameof(SecondaryId)); - jWriter.WriteValue(SecondaryId.Id); - jWriter.WritePropertyName(nameof(Variant)); - jWriter.WriteValue(Variant.Id); - jWriter.WritePropertyName(nameof(DefaultEntry)); - serializer.Serialize(jWriter, DefaultEntry); - jWriter.WritePropertyName("Options"); - jWriter.WriteStartArray(); - foreach (var option in OptionData) - { - jWriter.WriteStartObject(); - SubMod.WriteModOption(jWriter, option); - jWriter.WritePropertyName(nameof(option.AttributeMask)); - jWriter.WriteValue(option.AttributeMask); - jWriter.WriteEndObject(); - } - - jWriter.WriteEndArray(); - } - - public (int Redirections, int Swaps, int Manips) GetCounts() - => (0, 0, 1); - - public static ImcModGroup? Load(Mod mod, JObject json) - { - var options = json["Options"]; - var ret = new ImcModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - ObjectType = json[nameof(ObjectType)]?.ToObject() ?? ObjectType.Unknown, - BodySlot = json[nameof(BodySlot)]?.ToObject() ?? BodySlot.Unknown, - EquipSlot = json[nameof(EquipSlot)]?.ToObject() ?? EquipSlot.Unknown, - PrimaryId = new PrimaryId(json[nameof(PrimaryId)]?.ToObject() ?? 0), - SecondaryId = new SecondaryId(json[nameof(SecondaryId)]?.ToObject() ?? 0), - Variant = new Variant(json[nameof(Variant)]?.ToObject() ?? 0), - CanBeDisabled = json[nameof(CanBeDisabled)]?.ToObject() ?? false, - DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), - }; - if (ret.Name.Length == 0) - return null; - - if (options != null) - foreach (var child in options.Children()) - { - var subMod = new ImcSubMod(ret, child); - ret.OptionData.Add(subMod); - } - - if (!new ImcManipulation(ret.ObjectType, ret.BodySlot, ret.PrimaryId, ret.SecondaryId.Id, ret.Variant.Id, ret.EquipSlot, - ret.DefaultEntry).Validate(true)) - { - Penumbra.Messager.NotificationMessage($"Could not add IMC group because the associated IMC Entry is invalid.", - NotificationType.Warning); - return null; - } - - ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); - return ret; - } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 20021d29..f9fd532f 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -7,7 +7,6 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; namespace Penumbra.Mods.Manager.OptionEditor; @@ -15,13 +14,13 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ : ModOptionEditor(communicator, saveService, config), IService { /// Add a new, empty imc group with the given manipulation data. - public ImcModGroup? AddModGroup(Mod mod, string newName, ImcManipulation manip, SaveType saveType = SaveType.ImmediateSync) + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, SaveType saveType = SaveType.ImmediateSync) { if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) return null; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, manip, maxPriority); + var group = CreateGroup(mod, newName, identifier, defaultEntry, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -97,19 +96,14 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ }; - private static ImcModGroup CreateGroup(Mod mod, string newName, ImcManipulation manip, ModPriority priority, + private static ImcModGroup CreateGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { Name = newName, Priority = priority, - ObjectType = manip.ObjectType, - EquipSlot = manip.EquipSlot, - BodySlot = manip.BodySlot, - PrimaryId = manip.PrimaryId, - SecondaryId = manip.SecondaryId.Id, - Variant = manip.Variant, - DefaultEntry = manip.Entry, + Identifier = identifier, + DefaultEntry = defaultEntry, }; protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 969ad3fa..e1db0ccf 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -246,7 +246,7 @@ public class ModGroupEditor( { GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), _ => null, }; diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index 7f46bc95..c5c8f002 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -12,13 +12,23 @@ public class ImcSubMod(ImcModGroup group) : IModOption : this(group) { SubMod.LoadOptionData(json, this); - AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); + AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); + IsDisableSubMod = json[nameof(IsDisableSubMod)]?.ToObject() ?? false; } + public static ImcSubMod DisableSubMod(ImcModGroup group) + => new(group) + { + Name = "Disable", + AttributeMask = 0, + IsDisableSubMod = true, + }; + public Mod Mod => Group.Mod; public ushort AttributeMask; + public bool IsDisableSubMod { get; private init; } Mod IModOption.Mod => Mod; diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 19ae31a2..0c6648ba 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -101,7 +101,8 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(p => p.GetRequiredService().ImcChecker); private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 99889360..68933c9e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -19,9 +19,6 @@ public partial class ModEditWindow private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string PrimaryIdTooltip = - "Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; private const string EquipSlotTooltip = "Equip Slot"; private const string ModelRaceTooltip = "Model Race"; @@ -316,7 +313,7 @@ public partial class ModEditWindow private static class ImcRow { - private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; private static float IdWidth => 80 * UiHelpers.Scale; @@ -324,75 +321,60 @@ public partial class ModEditWindow private static float SmallIdWidth => 45 * UiHelpers.Scale; - /// Convert throwing to null-return if the file does not exist. - private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc) - { - try - { - return ImcFile.GetDefault(metaFileManager, imc.GamePath(), imc.EquipSlot, imc.Variant, out _); - } - catch - { - return null; - } - } - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(metaFileManager, _new); - var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; - defaultEntry ??= new ImcEntry(); + var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); + var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); + var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); + var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry.Value)); + editor.MetaEditor.Add(manip); // Identifier ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _new); + var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _new); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. - if (_new.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _new); + if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _new); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _new); + change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); ImGui.TableNextColumn(); - if (_new.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _new, 70); + if (_newIdentifier.ObjectType is ObjectType.DemiHuman) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); else ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); if (change) - _new = _new.Copy(GetDefault(metaFileManager, _new) ?? new ImcEntry()); + defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; // Values using var disabled = ImRaii.Disabled(); - - var entry = defaultEntry.Value; ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(entry, ref entry, false); + ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(entry, ref entry, false); + ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(entry, ref entry, false); + ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(entry, ref entry, false); + ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(entry, ref entry, false); + ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(entry, ref entry); + ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); } public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) @@ -439,10 +421,9 @@ public partial class ModEditWindow using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); ImGui.TableNextColumn(); - var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry(); + var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; var newEntry = meta.Entry; - - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); ImGui.SameLine(); changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 2d80d3df..3ac10cd0 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -5,7 +5,6 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -17,20 +16,20 @@ namespace Penumbra.UI.ModsTab.Groups; public class AddGroupDrawer : IUiService { private string _groupName = string.Empty; - private bool _groupNameValid = false; + private bool _groupNameValid; - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; - private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; + private ImcIdentifier _imcIdentifier = ImcIdentifier.Default; + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly ImcChecker _imcChecker; + private readonly ModManager _modManager; - public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker) { - _metaManager = metaManager; _modManager = modManager; + _imcChecker = imcChecker; UpdateEntry(); } @@ -61,7 +60,7 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } @@ -74,35 +73,35 @@ public class AddGroupDrawer : IUiService return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; + _groupName = string.Empty; _groupNameValid = false; } private void DrawImcInput(float width) { - var change = ImcManipulationDrawer.DrawObjectType(ref _imcManip, width); + var change = ImcManipulationDrawer.DrawObjectType(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcManip, width); - if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcIdentifier, width); + if (_imcIdentifier.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); } - else if (_imcManip.ObjectType is ObjectType.DemiHuman) + else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); } else { - change |= ImcManipulationDrawer.DrawSlot(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcManip, width); + change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); } if (change) @@ -125,8 +124,8 @@ public class AddGroupDrawer : IUiService : "Add a new multi selection option group to this mod."u8, width, !_groupNameValid || _entryInvalid)) { - _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry); + _groupName = string.Empty; _groupNameValid = false; } @@ -142,20 +141,7 @@ public class AddGroupDrawer : IUiService private void UpdateEntry() { - try - { - _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, - out _entryExists); - _imcFileExists = true; - } - catch (Exception) - { - _defaultEntry = new ImcEntry(); - _imcFileExists = false; - _entryExists = false; - } - - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); - _entryInvalid = !_imcManip.Validate(true); + (_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false); + _entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists; } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 2418c5cb..045149c9 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,10 +1,8 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; -using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Text; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; @@ -16,59 +14,75 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr { public void Draw() { + var identifier = group.Identifier; + var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var entry = group.DefaultEntry; + var changes = false; + + ImUtf8.TextFramed(identifier.ToString(), 0, editor.AvailableWidth, borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + using (ImUtf8.Group()) { - ImUtf8.Text("Object Type"u8); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text("Slot"u8); - ImUtf8.Text("Primary ID"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text("Secondary ID"); - ImUtf8.Text("Variant"u8); - ImUtf8.TextFrameAligned("Material ID"u8); - ImUtf8.TextFrameAligned("Material Animation ID"u8); - ImUtf8.TextFrameAligned("Decal ID"u8); ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + changes |= ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref entry, true); + } + + ImGui.SameLine(0, editor.PriorityWidth); + using (ImUtf8.Group()) + { + ImUtf8.TextFrameAligned("Material Animation ID"u8); ImUtf8.TextFrameAligned("Sound ID"u8); ImUtf8.TextFrameAligned("Can Be Disabled"u8); - ImUtf8.TextFrameAligned("Default Attributes"u8); } ImGui.SameLine(); + using (ImUtf8.Group()) + { + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref entry, true); + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); + } + + if (changes) + editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry); + + ImGui.Dummy(Vector2.Zero); + DrawOptions(); var attributeCache = new ImcAttributeCache(group); + DrawNewOption(attributeCache); + ImGui.Dummy(Vector2.Zero); + using (ImUtf8.Group()) { - ImUtf8.Text(group.ObjectType.ToName()); - if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) - ImUtf8.Text(group.EquipSlot.ToName()); - ImUtf8.Text($"{group.PrimaryId.Id}"); - if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) - ImUtf8.Text($"{group.SecondaryId.Id}"); - ImUtf8.Text($"{group.Variant.Id}"); - - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); - ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); - - var canBeDisabled = group.CanBeDisabled; - if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) - editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); - - var defaultDisabled = group.DefaultDisabled; - ImUtf8.SameLineInner(); - if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) - editor.ModManager.OptionEditor.ChangeModGroupDefaultOption(group, - group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); - - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + ImUtf8.TextFrameAligned("Default Attributes"u8); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + ImUtf8.TextFrameAligned(option.Name); } + ImUtf8.SameLineInner(); + using (ImUtf8.Group()) + { + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + } + private void DrawOptions() + { foreach (var (option, optionIdx) in group.OptionData.WithIndex()) { using var id = ImRaii.PushId(optionIdx); @@ -83,56 +97,51 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImUtf8.SameLineInner(); editor.DrawOptionDescription(option); - ImUtf8.SameLineInner(); - editor.DrawOptionDelete(option); - - ImUtf8.SameLineInner(); - ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); - - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + editor.OptionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); - } - - DrawNewOption(attributeCache); - return; - - static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) - { - for (var i = 0; i < ImcEntry.NumAttributes; ++i) + if (!option.IsDisableSubMod) { - using var id = ImRaii.PushId(i); - var value = (mask & 1 << i) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) - { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) - { - if (data is ImcModGroup g) - editor.ChangeDefaultAttribute(g, cache, i, value); - else - editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); - } - } - - ImUtf8.HoverTooltip($"{(char)('A' + i)}"); - if (i != 9) - ImUtf8.SameLineInner(); + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); } } } private void DrawNewOption(in ImcAttributeCache cache) { - if (cache.LowestUnsetMask == 0) - return; - - var name = editor.DrawNewOptionBase(group, group.Options.Count); + var dis = cache.LowestUnsetMask == 0; + var name = editor.DrawNewOptionBase(group, group.Options.Count); var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + var tt = dis + ? "No Free Attribute Slots for New Options..."u8 + : validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, !validName || dis)) { editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); editor.NewOptionName = null; } } + + private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & (1 << i)) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip("ABCDEFGHIJ"u8.Slice(i, 1)); + if (i != 9) + ImUtf8.SameLineInner(); + } + } } diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e7d70922..e8a27a74 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -7,6 +7,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -22,11 +23,16 @@ public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService + DescriptionEditPopup descriptionPopup, + ImcChecker imcChecker) : IUiService { - private static ReadOnlySpan DragDropLabel - => "##DragOption"u8; + private static ReadOnlySpan AcrossGroupsLabel + => "##DragOptionAcross"u8; + private static ReadOnlySpan InsideGroupLabel + => "##DragOptionInside"u8; + + internal readonly ImcChecker ImcChecker = imcChecker; internal readonly ModManager ModManager = modManager; internal readonly Queue ActionQueue = new(); @@ -50,6 +56,7 @@ public sealed class ModGroupEditDrawer( private IModGroup? _dragDropGroup; private IModOption? _dragDropOption; + private bool _draggingAcross; public void Draw(Mod mod) { @@ -292,32 +299,30 @@ public sealed class ModGroupEditDrawer( [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Source(IModOption option) { - if (option.Group is not ITexToolsGroup) - return; - using var source = ImUtf8.DragDropSource(); if (!source) return; - if (!DragDropSource.SetPayload(DragDropLabel)) + var across = option.Group is ITexToolsGroup; + + if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel)) { _dragDropGroup = option.Group; _dragDropOption = option; + _draggingAcross = across; } - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}..."); } private void Target(IModGroup group, int optionIdx) { - if (group is not ITexToolsGroup) - return; - - if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + if (_dragDropGroup != group + && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) return; using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + if (!target.Success || !DragDropTarget.CheckPayload(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) return; if (_dragDropGroup != null && _dragDropOption != null) @@ -342,6 +347,7 @@ public sealed class ModGroupEditDrawer( _dragDropGroup = null; _dragDropOption = null; + _draggingAcross = false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index 5873119e..c14652ac 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -1,8 +1,6 @@ using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using OtterGui.Text.HelperObjects; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -12,79 +10,78 @@ namespace Penumbra.UI.ModsTab; public static class ImcManipulationDrawer { - public static bool DrawObjectType(ref ImcManipulation manip, float width = 110) + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { - var ret = Combos.ImcType("##imcType", manip.ObjectType, out var type, width); + var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); ImUtf8.HoverTooltip("Object Type"u8); if (ret) { var equipSlot = type switch { - ObjectType.Equipment => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => manip.EquipSlot.IsEquipment() ? manip.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => manip.EquipSlot.IsAccessory() ? manip.EquipSlot : EquipSlot.Ears, + ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, _ => EquipSlot.Unknown, }; - manip = new ImcManipulation(type, manip.BodySlot, manip.PrimaryId, manip.SecondaryId == 0 ? 1 : manip.SecondaryId, - manip.Variant.Id, equipSlot, manip.Entry); + identifier = identifier with + { + EquipSlot = equipSlot, + SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + }; } return ret; } - public static bool DrawPrimaryId(ref ImcManipulation manip, float unscaledWidth = 80) + public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, manip.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - manip.PrimaryId.Id <= 1); + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + identifier.PrimaryId.Id <= 1); ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + "This should generally not be left <= 1 unless you explicitly want that."u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, newId, manip.SecondaryId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); + identifier = identifier with { PrimaryId = newId }; return ret; } - public static bool DrawSecondaryId(ref ImcManipulation manip, float unscaledWidth = 100) + public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, manip.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); ImUtf8.HoverTooltip("Secondary ID"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, newId, manip.Variant.Id, manip.EquipSlot, - manip.Entry); + identifier = identifier with { SecondaryId = newId }; return ret; } - public static bool DrawVariant(ref ImcManipulation manip, float unscaledWidth = 45) + public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) { - var ret = IdInput("##imcVariant"u8, unscaledWidth, manip.Variant.Id, out var newId, 0, byte.MaxValue, false); + var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); ImUtf8.HoverTooltip("Variant ID"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, (byte)newId, manip.EquipSlot, - manip.Entry); + identifier = identifier with { Variant = (byte)newId }; return ret; } - public static bool DrawSlot(ref ImcManipulation manip, float unscaledWidth = 100) + public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) { bool ret; EquipSlot slot; - switch (manip.ObjectType) + switch (identifier.ObjectType) { case ObjectType.Equipment: case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); break; case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", manip.EquipSlot, out slot, unscaledWidth); + ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); break; default: return false; } ImUtf8.HoverTooltip("Equip Slot"u8); if (ret) - manip = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, manip.Variant.Id, slot, - manip.Entry); + identifier = identifier with { EquipSlot = slot }; return ret; } From bad1f45ab91f54576da1041577eaeca83cb653ef Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 17:34:56 +0200 Subject: [PATCH 097/865] Use different hooking method for EQP entries. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 34 ++++++++++++++++++ Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 5 +-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 36 +++++++++++++++++++ .../Interop/Hooks/Meta/ModelLoadComplete.cs | 4 ++- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 6 ++-- Penumbra/Interop/PathResolving/MetaState.cs | 1 + 7 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/EqpHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ec35e664..539d1387 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ec35e66499eb388b4e7917e4fae4615218d33335 +Subproject commit 539d138700543e7c2c6c918f9f68e33228111e4d diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs new file mode 100644 index 00000000..457b9428 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -0,0 +1,34 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class EqpHook : FastHook +{ + public delegate void Delegate(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor); + + private readonly MetaState _metaState; + + public EqpHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, true); + } + + private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) + { + if (_metaState.EqpCollection.Valid) + { + using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection); + Task.Result.Original(utility, flags, armor); + } + else + { + Task.Result.Original(utility, flags, armor); + } + + Penumbra.Log.Excessive($"[GetEqpFlags] Invoked on 0x{(nint)utility:X} with 0x{(ulong)armor:X}, returned 0x{(ulong)*flags:X16}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index 8ffc050f..beae6acc 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -28,8 +29,8 @@ public sealed unsafe class GetEqpIndirect : FastHook return; Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var eqp = _metaState.ResolveEqpData(collection.ModCollection); + _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); Task.Result.Original(drawObject); + _metaState.EqpCollection = ResolveData.Invalid; } } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs new file mode 100644 index 00000000..89aaa9b0 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -0,0 +1,36 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class GetEqpIndirect2 : FastHook +{ + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public GetEqpIndirect2(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + { + _collectionResolver = collectionResolver; + _metaState = metaState; + Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, true); + } + + public delegate void Delegate(DrawObject* drawObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Detour(DrawObject* drawObject) + { + // Shortcut because this is also called all the time. + // Same thing is checked at the beginning of the original function. + if (((*(uint*)((nint)drawObject + Offsets.GetEqpIndirect2Skip) >> 0x12) & 1) == 0) + return; + + Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}."); + _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + Task.Result.Original(drawObject); + _metaState.EqpCollection = ResolveData.Invalid; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 9f191fdd..10c12594 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; @@ -23,8 +24,9 @@ public sealed unsafe class ModelLoadComplete : FastHook Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var eqp = _metaState.ResolveEqpData(collection.ModCollection); using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); - Task.Result.Original.Invoke(drawObject); + _metaState.EqpCollection = collection; + Task.Result.Original(drawObject); + _metaState.EqpCollection = ResolveData.Invalid; } } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 5f07ffc5..6fa5c263 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -46,6 +46,7 @@ public sealed unsafe class MetaState : IDisposable private readonly CreateCharacterBase _createCharacterBase; public ResolveData CustomizeChangeCollection = ResolveData.Invalid; + public ResolveData EqpCollection = ResolveData.Invalid; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; From e32e314863e46fb4b806bc9b2c3adf86a5ec2d0e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 May 2024 17:51:47 +0200 Subject: [PATCH 098/865] Add initial changelog for next release. --- Penumbra/UI/Changelog.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 8b00ab8d..add16f94 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -48,10 +48,34 @@ public class PenumbraChangelog Add8_3_0(Changelog); Add1_0_0_0(Changelog); Add1_0_2_0(Changelog); + Add1_0_3_0(Changelog); } #region Changelogs + private static void Add1_0_3_0(Changelog log) + => log.NextVersion("Version 1.0.3.0") + .RegisterHighlight("Collections now have associated GUIDs as identifiers instead of their names, so they can now be renamed.") + .RegisterEntry("Migrating those collections may introduce issues, please let me know as soon as possible if you encounter any.", 1) + .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) + .RegisterHighlight("A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") + .RegisterEntry("Mod Creators can add a IMC Group to their mod that controls a single IMC Manipulation, so they can provide options for the separate attributes for it.", 1) + .RegisterEntry("This makes it a lot easier to have combined options: No need for 'A', 'B' and 'AB', you can just define 'A' and 'B' and skip their combinations", 1) + .RegisterHighlight("Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") + .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) + .RegisterEntry("You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") + .RegisterHighlight("Added the option to display VFX for accessories if added via IMC edits, which the game does not do inherently (by Ocealot).") + .RegisterEntry("Added support for reading and writing the new material and model file formats from the benchmark.") + .RegisterEntry("Added the option to hide Machinist Offhands from the Changed Items tabs (because any change to it changes ALL of them), which is on by default.") + .RegisterEntry("Removed the auto-generated descriptions for newly created groups in Penumbra.") + .RegisterEntry("Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") + .RegisterEntry("Made a lot of further improvements on Model import/export (by ackwell).") + .RegisterEntry("Reworked the API and IPC structure heavily.") + .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") + .RegisterEntry("Fixed an issue with merging and deduplicating mods.") + .RegisterEntry("Fixed a crash when scanning for mods without access rights to the folder.") + .RegisterEntry("Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); + private static void Add1_0_2_0(Changelog log) => log.NextVersion("Version 1.0.2.0") .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") From f9527970cb610f8d8529478e9674d1bd32548bf9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 May 2024 00:43:02 +0200 Subject: [PATCH 099/865] Fix missing ID push for imc attributes. --- Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 045149c9..b10f123c 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -76,8 +76,11 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr using (ImUtf8.Group()) { DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); - foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + foreach (var (option, idx) in group.OptionData.WithIndex().Where(o => !o.Value.IsDisableSubMod)) + { + using var id = ImUtf8.PushId(idx); DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } } } From ed083f2a4ca375bf50eede01d72796408c320d7c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 May 2024 13:30:35 +0200 Subject: [PATCH 100/865] Add support for Global EQP Changes. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/CollectionCache.cs | 1 - Penumbra/Collections/Cache/MetaCache.cs | 48 ++++-- .../Collections/ModCollection.Cache.Access.cs | 4 + Penumbra/Import/TexToolsMeta.Export.cs | 3 + Penumbra/Import/TexToolsMeta.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 1 + Penumbra/Meta/Manipulations/GlobalEqpCache.cs | 80 +++++++++ .../Manipulations/GlobalEqpManipulation.cs | 50 ++++++ Penumbra/Meta/Manipulations/GlobalEqpType.cs | 61 +++++++ .../Meta/Manipulations/MetaManipulation.cs | 156 ++++++++++-------- Penumbra/Mods/Editor/ModMetaEditor.cs | 97 ++++++----- Penumbra/Mods/SubMods/IModDataContainer.cs | 13 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 66 ++++++++ Penumbra/Util/IdentifierExtensions.cs | 20 +++ 15 files changed, 471 insertions(+), 133 deletions(-) create mode 100644 Penumbra/Meta/Manipulations/GlobalEqpCache.cs create mode 100644 Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs create mode 100644 Penumbra/Meta/Manipulations/GlobalEqpType.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 539d1387..07cc26f1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 539d138700543e7c2c6c918f9f68e33228111e4d +Subproject commit 07cc26f196984a44711b3bc4c412947d863288bd diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index e2f20b46..4d8d0b4a 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -5,7 +5,6 @@ using Penumbra.Mods; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; -using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 4c147c3c..f42b72fc 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -4,7 +4,6 @@ using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; @@ -14,13 +13,14 @@ public class MetaCache : IDisposable, IEnumerable _manipulations = new(); - private EqpCache _eqpCache = new(); - private readonly EqdpCache _eqdpCache = new(); - private EstCache _estCache = new(); - private GmpCache _gmpCache = new(); - private CmpCache _cmpCache = new(); - private readonly ImcCache _imcCache = new(); + private readonly Dictionary _manipulations = new(); + private EqpCache _eqpCache = new(); + private readonly EqdpCache _eqdpCache = new(); + private EstCache _estCache = new(); + private GmpCache _gmpCache = new(); + private CmpCache _cmpCache = new(); + private readonly ImcCache _imcCache = new(); + private GlobalEqpCache _globalEqpCache = new(); public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) { @@ -69,6 +69,7 @@ public class MetaCache : IDisposable, IEnumerable _estCache.TemporarilySetFiles(_manager, type); + public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) + => _globalEqpCache.Apply(baseEntry, armor); + /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) @@ -193,8 +204,8 @@ public class MetaCache : IDisposable, IEnumerable _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, + MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), + MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), + MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), + MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), + MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), + MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), + MetaManipulation.Type.GlobalEqp => false, + MetaManipulation.Type.Unknown => false, + _ => false, } ? 1 : 0; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index b073e731..3f3733e0 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -8,6 +8,7 @@ using Penumbra.String.Classes; using Penumbra.Collections.Cache; using Penumbra.Interop.Services; using Penumbra.Mods.Editor; +using Penumbra.GameData.Structs; namespace Penumbra.Collections; @@ -114,4 +115,7 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); + + public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) + => _cache?.Meta.ApplyGlobalEqp(baseEntry, armor) ?? baseEntry; } diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 90ffaf60..03bdbd90 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -187,6 +187,9 @@ public partial class TexToolsMeta b.Write(manip.Gmp.Entry.UnknownTotal); } + break; + case MetaManipulation.Type.GlobalEqp: + // Not Supported break; } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 83b430fb..25e00bd7 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -44,7 +44,7 @@ public partial class TexToolsMeta var headerStart = reader.ReadUInt32(); reader.BaseStream.Seek(headerStart, SeekOrigin.Begin); - List<(MetaManipulation.Type type, uint offset, int size)> entries = new(); + List<(MetaManipulation.Type type, uint offset, int size)> entries = []; for (var i = 0; i < numHeaders; ++i) { var currentOffset = reader.BaseStream.Position; diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 457b9428..6663c211 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -23,6 +23,7 @@ public unsafe class EqpHook : FastHook { using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection); Task.Result.Original(utility, flags, armor); + *flags = _metaState.EqpCollection.ModCollection.ApplyGlobalEqp(*flags, armor); } else { diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs new file mode 100644 index 00000000..48ffb308 --- /dev/null +++ b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs @@ -0,0 +1,80 @@ +using OtterGui.Services; +using Penumbra.GameData.Structs; + +namespace Penumbra.Meta.Manipulations; + +public struct GlobalEqpCache : IService +{ + private readonly HashSet _doNotHideEarrings = []; + private readonly HashSet _doNotHideNecklace = []; + private readonly HashSet _doNotHideBracelets = []; + private readonly HashSet _doNotHideRingL = []; + private readonly HashSet _doNotHideRingR = []; + private bool _doNotHideVieraHats; + private bool _doNotHideHrothgarHats; + + public GlobalEqpCache() + { } + + public void Clear() + { + _doNotHideEarrings.Clear(); + _doNotHideNecklace.Clear(); + _doNotHideBracelets.Clear(); + _doNotHideRingL.Clear(); + _doNotHideRingR.Clear(); + _doNotHideHrothgarHats = false; + _doNotHideVieraHats = false; + } + + public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) + { + if (_doNotHideVieraHats) + original |= EqpEntry.HeadShowVieraHat; + + if (_doNotHideHrothgarHats) + original |= EqpEntry.HeadShowHrothgarHat; + + if (_doNotHideEarrings.Contains(armor[5].Set)) + original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman; + + if (_doNotHideNecklace.Contains(armor[6].Set)) + original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; + + if (_doNotHideBracelets.Contains(armor[7].Set)) + original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet; + + if (_doNotHideBracelets.Contains(armor[8].Set)) + original |= EqpEntry.HandShowRingR; + + if (_doNotHideBracelets.Contains(armor[9].Set)) + original |= EqpEntry.HandShowRingL; + return original; + } + + public bool Add(GlobalEqpManipulation manipulation) + => manipulation.Type switch + { + GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition), + GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition), + GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Add(manipulation.Condition), + GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition), + GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), + GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), + GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), + _ => false, + }; + + public bool Remove(GlobalEqpManipulation manipulation) + => manipulation.Type switch + { + GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideBracelets => _doNotHideBracelets.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), + GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), + GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), + _ => false, + }; +} diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs new file mode 100644 index 00000000..ada543dc --- /dev/null +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -0,0 +1,50 @@ +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly struct GlobalEqpManipulation : IMetaManipulation +{ + public GlobalEqpType Type { get; init; } + public PrimaryId Condition { get; init; } + + public bool Validate() + { + if (!Enum.IsDefined(Type)) + return false; + + if (Type is GlobalEqpType.DoNotHideVieraHats or GlobalEqpType.DoNotHideHrothgarHats) + return Condition == 0; + + return Condition != 0; + } + + + public bool Equals(GlobalEqpManipulation other) + => Type == other.Type + && Condition.Equals(other.Condition); + + public int CompareTo(GlobalEqpManipulation other) + { + var typeComp = Type.CompareTo(other); + return typeComp != 0 ? typeComp : Condition.Id.CompareTo(other.Condition.Id); + } + + public override bool Equals(object? obj) + => obj is GlobalEqpManipulation other && Equals(other); + + public override int GetHashCode() + => HashCode.Combine((int)Type, Condition); + + public static bool operator ==(GlobalEqpManipulation left, GlobalEqpManipulation right) + => left.Equals(right); + + public static bool operator !=(GlobalEqpManipulation left, GlobalEqpManipulation right) + => !left.Equals(right); + + public override string ToString() + => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; + + public MetaIndex FileIndex() + => (MetaIndex)(-1); +} diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs new file mode 100644 index 00000000..57d99d56 --- /dev/null +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -0,0 +1,61 @@ +namespace Penumbra.Meta.Manipulations; + +public enum GlobalEqpType +{ + DoNotHideEarrings, + DoNotHideNecklace, + DoNotHideBracelets, + DoNotHideRingR, + DoNotHideRingL, + DoNotHideHrothgarHats, + DoNotHideVieraHats, +} + +public static class GlobalEqpExtensions +{ + public static bool HasCondition(this GlobalEqpType type) + => type switch + { + GlobalEqpType.DoNotHideEarrings => true, + GlobalEqpType.DoNotHideNecklace => true, + GlobalEqpType.DoNotHideBracelets => true, + GlobalEqpType.DoNotHideRingR => true, + GlobalEqpType.DoNotHideRingL => true, + GlobalEqpType.DoNotHideHrothgarHats => false, + GlobalEqpType.DoNotHideVieraHats => false, + _ => false, + }; + + + public static ReadOnlySpan ToName(this GlobalEqpType type) + => type switch + { + GlobalEqpType.DoNotHideEarrings => "Do Not Hide Earrings"u8, + GlobalEqpType.DoNotHideNecklace => "Do Not Hide Necklaces"u8, + GlobalEqpType.DoNotHideBracelets => "Do Not Hide Bracelets"u8, + GlobalEqpType.DoNotHideRingR => "Do Not Hide Rings (Right Finger)"u8, + GlobalEqpType.DoNotHideRingL => "Do Not Hide Rings (Left Finger)"u8, + GlobalEqpType.DoNotHideHrothgarHats => "Do Not Hide Hats for Hrothgar"u8, + GlobalEqpType.DoNotHideVieraHats => "Do Not Hide Hats for Viera"u8, + _ => "\0"u8, + }; + + public static ReadOnlySpan ToDescription(this GlobalEqpType type) + => type switch + { + GlobalEqpType.DoNotHideEarrings => "Prevents the game from hiding earrings through other models when a specific earring is worn."u8, + GlobalEqpType.DoNotHideNecklace => + "Prevents the game from hiding necklaces through other models when a specific necklace is worn."u8, + GlobalEqpType.DoNotHideBracelets => + "Prevents the game from hiding bracelets through other models when a specific bracelet is worn."u8, + GlobalEqpType.DoNotHideRingR => + "Prevents the game from hiding rings worn on the right finger through other models when a specific ring is worn on the right finger."u8, + GlobalEqpType.DoNotHideRingL => + "Prevents the game from hiding rings worn on the left finger through other models when a specific ring is worn on the left finger."u8, + GlobalEqpType.DoNotHideHrothgarHats => + "Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8, + GlobalEqpType.DoNotHideVieraHats => + "Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8, + _ => "\0"u8, + }; +} diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index ed184d52..f22de809 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -21,13 +21,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara public enum Type : byte { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5, - Rsp = 6, + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + Rsp = 6, + GlobalEqp = 7, } [FieldOffset(0)] @@ -54,6 +55,10 @@ public readonly struct MetaManipulation : IEquatable, ICompara [JsonIgnore] public readonly ImcManipulation Imc = default; + [FieldOffset(0)] + [JsonIgnore] + public readonly GlobalEqpManipulation GlobalEqp = default; + [FieldOffset(15)] [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("Type")] @@ -63,14 +68,15 @@ public readonly struct MetaManipulation : IEquatable, ICompara { get => ManipulationType switch { - Type.Unknown => null, - Type.Imc => Imc, - Type.Eqdp => Eqdp, - Type.Eqp => Eqp, - Type.Est => Est, - Type.Gmp => Gmp, - Type.Rsp => Rsp, - _ => null, + Type.Unknown => null, + Type.Imc => Imc, + Type.Eqdp => Eqdp, + Type.Eqp => Eqp, + Type.Est => Est, + Type.Gmp => Gmp, + Type.Rsp => Rsp, + Type.GlobalEqp => GlobalEqp, + _ => null, }; init { @@ -100,6 +106,10 @@ public readonly struct MetaManipulation : IEquatable, ICompara Imc = m; ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; return; + case GlobalEqpManipulation m: + GlobalEqp = m; + ManipulationType = m.Validate() ? Type.GlobalEqp : Type.Unknown; + return; } } } @@ -108,13 +118,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara { return ManipulationType switch { - Type.Imc => Imc.Validate(true), - Type.Eqdp => Eqdp.Validate(), - Type.Eqp => Eqp.Validate(), - Type.Est => Est.Validate(), - Type.Gmp => Gmp.Validate(), - Type.Rsp => Rsp.Validate(), - _ => false, + Type.Imc => Imc.Validate(true), + Type.Eqdp => Eqdp.Validate(), + Type.Eqp => Eqp.Validate(), + Type.Est => Est.Validate(), + Type.Gmp => Gmp.Validate(), + Type.Rsp => Rsp.Validate(), + Type.GlobalEqp => GlobalEqp.Validate(), + _ => false, }; } @@ -154,6 +165,12 @@ public readonly struct MetaManipulation : IEquatable, ICompara ManipulationType = Type.Imc; } + public MetaManipulation(GlobalEqpManipulation eqp) + { + GlobalEqp = eqp; + ManipulationType = Type.GlobalEqp; + } + public static implicit operator MetaManipulation(EqpManipulation eqp) => new(eqp); @@ -172,6 +189,9 @@ public readonly struct MetaManipulation : IEquatable, ICompara public static implicit operator MetaManipulation(ImcManipulation imc) => new(imc); + public static implicit operator MetaManipulation(GlobalEqpManipulation eqp) + => new(eqp); + public bool EntryEquals(MetaManipulation other) { if (ManipulationType != other.ManipulationType) @@ -179,13 +199,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara return ManipulationType switch { - Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), - Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), - Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), - Type.Est => Est.Entry.Equals(other.Est.Entry), - Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), - Type.Imc => Imc.Entry.Equals(other.Imc.Entry), - _ => throw new ArgumentOutOfRangeException(), + Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), + Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), + Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), + Type.Est => Est.Entry.Equals(other.Est.Entry), + Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), + Type.Imc => Imc.Entry.Equals(other.Imc.Entry), + Type.GlobalEqp => true, + _ => throw new ArgumentOutOfRangeException(), }; } @@ -196,13 +217,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara return ManipulationType switch { - Type.Eqp => Eqp.Equals(other.Eqp), - Type.Gmp => Gmp.Equals(other.Gmp), - Type.Eqdp => Eqdp.Equals(other.Eqdp), - Type.Est => Est.Equals(other.Est), - Type.Rsp => Rsp.Equals(other.Rsp), - Type.Imc => Imc.Equals(other.Imc), - _ => false, + Type.Eqp => Eqp.Equals(other.Eqp), + Type.Gmp => Gmp.Equals(other.Gmp), + Type.Eqdp => Eqdp.Equals(other.Eqdp), + Type.Est => Est.Equals(other.Est), + Type.Rsp => Rsp.Equals(other.Rsp), + Type.Imc => Imc.Equals(other.Imc), + Type.GlobalEqp => GlobalEqp.Equals(other.GlobalEqp), + _ => false, }; } @@ -213,13 +235,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara return ManipulationType switch { - Type.Eqp => Eqp.Copy(other.Eqp.Entry), - Type.Gmp => Gmp.Copy(other.Gmp.Entry), - Type.Eqdp => Eqdp.Copy(other.Eqdp), - Type.Est => Est.Copy(other.Est.Entry), - Type.Rsp => Rsp.Copy(other.Rsp.Entry), - Type.Imc => Imc.Copy(other.Imc.Entry), - _ => throw new ArgumentOutOfRangeException(), + Type.Eqp => Eqp.Copy(other.Eqp.Entry), + Type.Gmp => Gmp.Copy(other.Gmp.Entry), + Type.Eqdp => Eqdp.Copy(other.Eqdp), + Type.Est => Est.Copy(other.Est.Entry), + Type.Rsp => Rsp.Copy(other.Rsp.Entry), + Type.Imc => Imc.Copy(other.Imc.Entry), + Type.GlobalEqp => GlobalEqp, + _ => throw new ArgumentOutOfRangeException(), }; } @@ -229,13 +252,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara public override int GetHashCode() => ManipulationType switch { - Type.Eqp => Eqp.GetHashCode(), - Type.Gmp => Gmp.GetHashCode(), - Type.Eqdp => Eqdp.GetHashCode(), - Type.Est => Est.GetHashCode(), - Type.Rsp => Rsp.GetHashCode(), - Type.Imc => Imc.GetHashCode(), - _ => 0, + Type.Eqp => Eqp.GetHashCode(), + Type.Gmp => Gmp.GetHashCode(), + Type.Eqdp => Eqdp.GetHashCode(), + Type.Est => Est.GetHashCode(), + Type.Rsp => Rsp.GetHashCode(), + Type.Imc => Imc.GetHashCode(), + Type.GlobalEqp => GlobalEqp.GetHashCode(), + _ => 0, }; public unsafe int CompareTo(MetaManipulation other) @@ -249,13 +273,14 @@ public readonly struct MetaManipulation : IEquatable, ICompara public override string ToString() => ManipulationType switch { - Type.Eqp => Eqp.ToString(), - Type.Gmp => Gmp.ToString(), - Type.Eqdp => Eqdp.ToString(), - Type.Est => Est.ToString(), - Type.Rsp => Rsp.ToString(), - Type.Imc => Imc.ToString(), - _ => "Invalid", + Type.Eqp => Eqp.ToString(), + Type.Gmp => Gmp.ToString(), + Type.Eqdp => Eqdp.ToString(), + Type.Est => Est.ToString(), + Type.Rsp => Rsp.ToString(), + Type.Imc => Imc.ToString(), + Type.GlobalEqp => GlobalEqp.ToString(), + _ => "Invalid", }; public string EntryToString() @@ -263,14 +288,15 @@ public readonly struct MetaManipulation : IEquatable, ICompara { Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", - Type.Eqp => $"{(ulong)Eqp.Entry:X}", - Type.Est => $"{Est.Entry}", - Type.Gmp => $"{Gmp.Entry.Value}", - Type.Rsp => $"{Rsp.Entry}", - _ => string.Empty, - }; - + Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", + Type.Eqp => $"{(ulong)Eqp.Entry:X}", + Type.Est => $"{Est.Entry}", + Type.Gmp => $"{Gmp.Entry.Value}", + Type.Rsp => $"{Rsp.Entry}", + Type.GlobalEqp => string.Empty, + _ => string.Empty, + }; + public static bool operator ==(MetaManipulation left, MetaManipulation right) => left.Equals(right); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 2f7fd04c..829161f5 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -6,19 +6,21 @@ namespace Penumbra.Mods.Editor; public class ModMetaEditor(ModManager modManager) { - private readonly HashSet _imc = []; - private readonly HashSet _eqp = []; - private readonly HashSet _eqdp = []; - private readonly HashSet _gmp = []; - private readonly HashSet _est = []; - private readonly HashSet _rsp = []; + private readonly HashSet _imc = []; + private readonly HashSet _eqp = []; + private readonly HashSet _eqdp = []; + private readonly HashSet _gmp = []; + private readonly HashSet _est = []; + private readonly HashSet _rsp = []; + private readonly HashSet _globalEqp = []; - public int OtherImcCount { get; private set; } - public int OtherEqpCount { get; private set; } - public int OtherEqdpCount { get; private set; } - public int OtherGmpCount { get; private set; } - public int OtherEstCount { get; private set; } - public int OtherRspCount { get; private set; } + public int OtherImcCount { get; private set; } + public int OtherEqpCount { get; private set; } + public int OtherEqdpCount { get; private set; } + public int OtherGmpCount { get; private set; } + public int OtherEstCount { get; private set; } + public int OtherRspCount { get; private set; } + public int OtherGlobalEqpCount { get; private set; } public bool Changes { get; private set; } @@ -40,17 +42,21 @@ public class ModMetaEditor(ModManager modManager) public IReadOnlySet Rsp => _rsp; + public IReadOnlySet GlobalEqp + => _globalEqp; + public bool CanAdd(MetaManipulation m) { return m.ManipulationType switch { - MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), - MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), - MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), - MetaManipulation.Type.Est => !_est.Contains(m.Est), - MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), - MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), - _ => false, + MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), + MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), + MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), + MetaManipulation.Type.Est => !_est.Contains(m.Est), + MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), + MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), + MetaManipulation.Type.GlobalEqp => !_globalEqp.Contains(m.GlobalEqp), + _ => false, }; } @@ -58,13 +64,14 @@ public class ModMetaEditor(ModManager modManager) { var added = m.ManipulationType switch { - MetaManipulation.Type.Imc => _imc.Add(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), - MetaManipulation.Type.Est => _est.Add(m.Est), - MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), - _ => false, + MetaManipulation.Type.Imc => _imc.Add(m.Imc), + MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), + MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), + MetaManipulation.Type.Est => _est.Add(m.Est), + MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), + MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), + MetaManipulation.Type.GlobalEqp => _globalEqp.Add(m.GlobalEqp), + _ => false, }; Changes |= added; return added; @@ -74,13 +81,14 @@ public class ModMetaEditor(ModManager modManager) { var deleted = m.ManipulationType switch { - MetaManipulation.Type.Imc => _imc.Remove(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), - MetaManipulation.Type.Est => _est.Remove(m.Est), - MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), - _ => false, + MetaManipulation.Type.Imc => _imc.Remove(m.Imc), + MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), + MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), + MetaManipulation.Type.Est => _est.Remove(m.Est), + MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), + MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), + MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(m.GlobalEqp), + _ => false, }; Changes |= deleted; return deleted; @@ -100,17 +108,19 @@ public class ModMetaEditor(ModManager modManager) _gmp.Clear(); _est.Clear(); _rsp.Clear(); + _globalEqp.Clear(); Changes = true; } public void Load(Mod mod, IModDataContainer currentOption) { - OtherImcCount = 0; - OtherEqpCount = 0; - OtherEqdpCount = 0; - OtherGmpCount = 0; - OtherEstCount = 0; - OtherRspCount = 0; + OtherImcCount = 0; + OtherEqpCount = 0; + OtherEqdpCount = 0; + OtherGmpCount = 0; + OtherEstCount = 0; + OtherRspCount = 0; + OtherGlobalEqpCount = 0; foreach (var option in mod.AllDataContainers) { if (option == currentOption) @@ -138,6 +148,9 @@ public class ModMetaEditor(ModManager modManager) case MetaManipulation.Type.Rsp: ++OtherRspCount; break; + case MetaManipulation.Type.GlobalEqp: + ++OtherGlobalEqpCount; + break; } } } @@ -179,6 +192,9 @@ public class ModMetaEditor(ModManager modManager) case MetaManipulation.Type.Rsp: _rsp.Add(manip.Rsp); break; + case MetaManipulation.Type.GlobalEqp: + _globalEqp.Add(manip.GlobalEqp); + break; } } @@ -191,5 +207,6 @@ public class ModMetaEditor(ModManager modManager) .Concat(_eqp.Select(m => (MetaManipulation)m)) .Concat(_est.Select(m => (MetaManipulation)m)) .Concat(_gmp.Select(m => (MetaManipulation)m)) - .Concat(_rsp.Select(m => (MetaManipulation)m)); + .Concat(_rsp.Select(m => (MetaManipulation)m)) + .Concat(_globalEqp.Select(m => (MetaManipulation)m)); } diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index a6ab491f..7f7ef4a6 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -5,17 +5,16 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.SubMods; - public interface IModDataContainer { - public IMod Mod { get; } + public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } - public string GetName(); - public string GetFullName(); + public string GetName(); + public string GetFullName(); public (int GroupIndex, int DataIndex) GetDataIndices(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 68933c9e..7cf75c03 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; @@ -75,6 +76,8 @@ public partial class ModEditWindow _editor.MetaEditor.OtherGmpCount); DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, _editor.MetaEditor.OtherRspCount); + DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, + GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherGlobalEqpCount); } @@ -702,6 +705,69 @@ public partial class ModEditWindow } } + private static class GlobalEqpRow + { + private static GlobalEqpManipulation _new = new() + { + Type = GlobalEqpType.DoNotHideEarrings, + Condition = 1, + }; + + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard.", iconSize, + editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(_new); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(250 * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##geqpType"u8, _new.Type.ToName())) + { + if (combo) + foreach (var type in Enum.GetValues()) + { + if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) + _new = new GlobalEqpManipulation + { + Type = type, + Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, + }; + ImUtf8.HoverTooltip(type.ToDescription()); + } + } + + ImUtf8.HoverTooltip(_new.Type.ToDescription()); + + ImGui.TableNextColumn(); + if (!_new.Type.HasCondition()) + return; + + if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) + _new = _new with { Condition = newId }; + } + + public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImUtf8.Text(meta.Type.ToName()); + ImUtf8.HoverTooltip(meta.Type.ToDescription()); + ImGui.TableNextColumn(); + if (meta.Type.HasCondition()) + { + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImUtf8.Text($"{meta.Condition.Id}"); + } + } + } + // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border) diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 392a5aba..7368c7c8 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -76,6 +76,26 @@ public static class IdentifierExtensions case MetaManipulation.Type.Rsp: changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); break; + case MetaManipulation.Type.GlobalEqp: + var path = manip.GlobalEqp.Type switch + { + GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.Ears), + GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.Neck), + GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.Wrists), + GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.RFinger), + GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, + EquipSlot.LFinger), + GlobalEqpType.DoNotHideHrothgarHats => string.Empty, + GlobalEqpType.DoNotHideVieraHats => string.Empty, + _ => string.Empty, + }; + if (path.Length > 0) + identifier.Identify(changedItems, path); + break; } } From cd133bddbba7241138829ce7f7f7a45a8e50b98f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 May 2024 14:50:22 +0200 Subject: [PATCH 101/865] Update Changelog. --- Penumbra/UI/Changelog.cs | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index add16f94..2b2cfa99 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -55,26 +55,46 @@ public class PenumbraChangelog private static void Add1_0_3_0(Changelog log) => log.NextVersion("Version 1.0.3.0") + .RegisterImportant( + "This update comes, again, with a lot of very heavy backend changes (collections and groups) and thus may introduce new issues.") .RegisterHighlight("Collections now have associated GUIDs as identifiers instead of their names, so they can now be renamed.") .RegisterEntry("Migrating those collections may introduce issues, please let me know as soon as possible if you encounter any.", 1) - .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) - .RegisterHighlight("A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") - .RegisterEntry("Mod Creators can add a IMC Group to their mod that controls a single IMC Manipulation, so they can provide options for the separate attributes for it.", 1) - .RegisterEntry("This makes it a lot easier to have combined options: No need for 'A', 'B' and 'AB', you can just define 'A' and 'B' and skip their combinations", 1) - .RegisterHighlight("Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") + .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) + .RegisterHighlight( + "A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") + .RegisterEntry( + "Mod Creators can add a IMC Group to their mod that controls a single IMC Manipulation, so they can provide options for the separate attributes for it.", + 1) + .RegisterEntry( + "This makes it a lot easier to have combined options: No need for 'A', 'B' and 'AB', you can just define 'A' and 'B' and skip their combinations", + 1) + .RegisterHighlight("A new type of Meta Manipulation was added, 'Global EQP Manipulation'.") + .RegisterEntry( + "Global EQP Manipulations allow accessories to make other equipment pieces not hide them, e.g. whenever a character is wearing a specific Bracelet, neither body nor hand items will ever hide bracelets.", + 1) + .RegisterEntry( + "This can be used if something like a jacket or a stole is put onto an accessory to prevent it from being hidden in general.", + 1) + .RegisterHighlight( + "Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) - .RegisterEntry("You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") - .RegisterHighlight("Added the option to display VFX for accessories if added via IMC edits, which the game does not do inherently (by Ocealot).") + .RegisterEntry( + "You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") + .RegisterHighlight( + "Added the option to display VFX for accessories if added via IMC edits, which the game does not do inherently (by Ocealot).") .RegisterEntry("Added support for reading and writing the new material and model file formats from the benchmark.") - .RegisterEntry("Added the option to hide Machinist Offhands from the Changed Items tabs (because any change to it changes ALL of them), which is on by default.") + .RegisterEntry( + "Added the option to hide Machinist Offhands from the Changed Items tabs (because any change to it changes ALL of them), which is on by default.") .RegisterEntry("Removed the auto-generated descriptions for newly created groups in Penumbra.") - .RegisterEntry("Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") + .RegisterEntry( + "Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") .RegisterEntry("Made a lot of further improvements on Model import/export (by ackwell).") .RegisterEntry("Reworked the API and IPC structure heavily.") .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") .RegisterEntry("Fixed an issue with merging and deduplicating mods.") .RegisterEntry("Fixed a crash when scanning for mods without access rights to the folder.") - .RegisterEntry("Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); + .RegisterEntry( + "Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); private static void Add1_0_2_0(Changelog log) => log.NextVersion("Version 1.0.2.0") From 1d230050c214df15aeb83e7f14b110a3e836a4eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 26 May 2024 15:41:27 +0200 Subject: [PATCH 102/865] Fix typo and rename geqp options. --- Penumbra/Meta/Manipulations/GlobalEqpType.cs | 14 +++++++------- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs index 57d99d56..d57af1d9 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpType.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -30,13 +30,13 @@ public static class GlobalEqpExtensions public static ReadOnlySpan ToName(this GlobalEqpType type) => type switch { - GlobalEqpType.DoNotHideEarrings => "Do Not Hide Earrings"u8, - GlobalEqpType.DoNotHideNecklace => "Do Not Hide Necklaces"u8, - GlobalEqpType.DoNotHideBracelets => "Do Not Hide Bracelets"u8, - GlobalEqpType.DoNotHideRingR => "Do Not Hide Rings (Right Finger)"u8, - GlobalEqpType.DoNotHideRingL => "Do Not Hide Rings (Left Finger)"u8, - GlobalEqpType.DoNotHideHrothgarHats => "Do Not Hide Hats for Hrothgar"u8, - GlobalEqpType.DoNotHideVieraHats => "Do Not Hide Hats for Viera"u8, + GlobalEqpType.DoNotHideEarrings => "Always Show Earrings"u8, + GlobalEqpType.DoNotHideNecklace => "Always Show Necklaces"u8, + GlobalEqpType.DoNotHideBracelets => "Always Show Bracelets"u8, + GlobalEqpType.DoNotHideRingR => "Always Show Rings (Right Finger)"u8, + GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8, + GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8, + GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8, _ => "\0"u8, }; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 7cf75c03..6f542377 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -76,7 +76,7 @@ public partial class ModEditWindow _editor.MetaEditor.OtherGmpCount); DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, _editor.MetaEditor.OtherRspCount); - DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, + DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherGlobalEqpCount); } From ca777ba1bf6369f7fd6202f40620d882d56b4e86 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 May 2024 17:43:13 +0200 Subject: [PATCH 103/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 07cc26f1..b8282970 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07cc26f196984a44711b3bc4c412947d863288bd +Subproject commit b8282970ee78a2c085e740f60450fecf7ea58b9c From fe266dca314f873422ec3f87a4769cbdc7dd520a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 27 May 2024 15:45:29 +0000 Subject: [PATCH 104/865] [CI] Updating repo.json for testing_1.0.3.0 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 232afaa0..2327420d 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.2.6", + "TestingAssemblyVersion": "1.0.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.2.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From f11cefcec1e21d416d25f7cbb1a6f31d8a19705f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 00:11:31 +0200 Subject: [PATCH 105/865] Fix best match fullpath returning broken FullPath instead of nullopt. --- Penumbra/Mods/Groups/MultiModGroup.cs | 11 ++++++++--- Penumbra/Mods/Groups/SingleModGroup.cs | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 38c0ef15..7816d628 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; +using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -40,9 +41,13 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => OptionData.Count > 0; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => OptionData.OrderByDescending(o => o.Priority) - .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) - .FirstOrDefault(); + { + foreach (var path in OptionData.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))) + return path; + + return null; + } public IModOption? AddOption(string name, string description = "") { diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 49190e34..a6ebd846 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -30,9 +30,13 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public readonly List OptionData = []; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => OptionData - .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) - .FirstOrDefault(); + { + foreach (var path in OptionData + .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file))) + return path; + + return null; + } public IModOption AddOption(string name, string description = "") { From b30de460e729c4d0eb6ccdfcab298738f10ef54a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 00:11:54 +0200 Subject: [PATCH 106/865] Fix ColorTable preview with legacy color tables. --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 4 ++-- .../LiveColorTablePreviewer.cs | 2 +- .../ModEditWindow.Materials.ColorTable.cs | 13 ++++++------ .../ModEditWindow.Materials.MtrlTab.cs | 21 +++++++++++-------- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b8282970..b83ce830 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b8282970ee78a2c085e740f60450fecf7ea58b9c +Subproject commit b83ce830919ca56e8b066d48d588c889df3af39b diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 73a5e725..5df9e1c1 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = material.Mtrl.Table; + var table = new LegacyColorTable(material.Mtrl.Table); var normal = material.Textures[TextureUsage.SamplerNormal]; @@ -103,7 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - private readonly struct ProcessCharacterNormalOperation(Image normal, ColorTable table) : IRowOperation + private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); public Image BaseColor { get; } = new(normal.Width, normal.Height); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index f211e0bc..8e75a895 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -8,7 +8,7 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { public const int TextureWidth = 4; - public const int TextureHeight = GameData.Files.MaterialStructs.ColorTable.NumUsedRows; + public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows; public const int TextureLength = TextureWidth * TextureHeight * 4; private readonly IFramework _framework; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 54c0eff6..15bd7cc9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -116,7 +116,7 @@ public partial class ModEditWindow { var ret = false; if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < ColorTable.NumUsedRows; ++i) + for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); tab.UpdateColorTablePreview(); @@ -170,6 +170,7 @@ public partial class ModEditWindow } } + [SkipLocalsInit] private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, @@ -178,11 +179,11 @@ public partial class ModEditWindow try { - var data = new byte[ColorTable.Row.Size + 2]; + Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, 2); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); } var text = Convert.ToBase64String(data); @@ -218,7 +219,7 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + 2 + if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size || !tab.Mtrl.HasTable) return false; @@ -349,7 +350,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); tmpFloat = row.GlossStrength; ImGui.SetNextItemWidth(floatSize); - float glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") && FixFloat(ref tmpFloat, row.GlossStrength)) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 9421493e..56e9482b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -455,7 +455,8 @@ public partial class ModEditWindow { UnbindFromMaterialInstances(); - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), FilePath); + var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), + FilePath); var foundMaterials = new HashSet(); foreach (var materialInfo in instances) @@ -596,13 +597,13 @@ public partial class ModEditWindow if (!Mtrl.HasTable) return; - var row = Mtrl.Table[rowIdx]; + var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; - var dye = Mtrl.DyeTable[rowIdx]; + var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes, default); + row.ApplyDyeTemplate(dye, dyes); } if (HighlightedColorTableRow == rowIdx) @@ -624,17 +625,18 @@ public partial class ModEditWindow if (!Mtrl.HasTable) return; - var rows = Mtrl.Table; + var rows = new LegacyColorTable(Mtrl.Table); + var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < ColorTable.NumUsedRows; ++i) + for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) { ref var row = ref rows[i]; - var dye = Mtrl.DyeTable[i]; + var dye = dyeRows[i]; if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes, default); + row.ApplyDyeTemplate(dye, dyes); } } @@ -643,12 +645,13 @@ public partial class ModEditWindow foreach (var previewer in ColorTablePreviewers) { + // TODO: Dawntrail rows.AsHalves().CopyTo(previewer.ColorTable); previewer.ScheduleUpdate(); } } - private static void ApplyHighlight(ref ColorTable.Row row, float time) + private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); From eb2a9b810922f409f8630e73a75c63353f1e709f Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 27 May 2024 22:13:55 +0000 Subject: [PATCH 107/865] [CI] Updating repo.json for testing_1.0.3.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2327420d..1cf9c104 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.3.0", + "TestingAssemblyVersion": "1.0.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 255d11974fcad8893e009766b9c8254be12ec9eb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 11:20:17 +0200 Subject: [PATCH 108/865] Fix IMC Stupid. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 01bf112b..c95884c6 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -204,7 +204,7 @@ public class FileEditor( private void SaveButton() { - var canSave = _changed && _currentFile != null && _currentFile.Valid; + var canSave = _changed && _currentFile is { Valid: true }; if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, $"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave)) { diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index c14652ac..694ae11c 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -26,6 +26,7 @@ public static class ImcManipulationDrawer }; identifier = identifier with { + ObjectType = type, EquipSlot = equipSlot, SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, }; From 5d1b17f96d159276e3470e462b56fbd2b101dd6e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 12:51:12 +0200 Subject: [PATCH 109/865] Fix mdl imports not being savable. --- Penumbra.GameData | 2 +- Penumbra/Import/Models/Import/ModelImporter.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b83ce830..f2cea65b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b83ce830919ca56e8b066d48d588c889df3af39b +Subproject commit f2cea65b83b2d6cb0d03339e8f76aed8102a41d5 diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index eedd12ab..a141d754 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -104,6 +104,7 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) Radius = 1, BoneBoundingBoxes = Enumerable.Repeat(MdlFile.EmptyBoundingBox, _bones.Count).ToArray(), RemainingData = [.._vertexBuffer, ..indexBuffer], + Valid = true, }; } From f5d6ac8bdbd43edf11f5d7ed41d7f587c49590d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 12:51:28 +0200 Subject: [PATCH 110/865] Fix Remove Assignment being visible for base and interface. --- Penumbra/Collections/Manager/CollectionType.cs | 3 +++ Penumbra/UI/CollectionTab/CollectionPanel.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs index 8c51fd90..c25413b8 100644 --- a/Penumbra/Collections/Manager/CollectionType.cs +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -107,6 +107,9 @@ public static class CollectionTypeExtensions public static bool IsSpecial(this CollectionType collectionType) => collectionType < CollectionType.Default; + public static bool CanBeRemoved(this CollectionType collectionType) + => collectionType.IsSpecial() || collectionType is CollectionType.Individual; + public static readonly (CollectionType, string, string)[] Special = Enum.GetValues() .Where(IsSpecial) .Select(s => (s, s.ToName(), s.ToDescription())) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 8625335e..bb22e6a7 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -287,7 +287,7 @@ public sealed class CollectionPanel : IDisposable _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); } - if (collection != null) + if (collection != null && type.CanBeRemoved()) { using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); if (ImGui.MenuItem("Remove this assignment.")) From 8891ea057086094e56220b60989741d7fc36dfc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 28 May 2024 12:53:02 +0200 Subject: [PATCH 111/865] Fix imc identifiers setting equip slot to something where they should not. --- Penumbra/Meta/Manipulations/ImcManipulation.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index 945aab04..eb3720c9 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -64,10 +64,8 @@ public readonly struct ImcManipulation : IMetaManipulation { ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, - equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot == EquipSlot.Unknown ? EquipSlot.Head : equipSlot, - bodySlot), + ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), + _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, bodySlot == BodySlot.Unknown ? BodySlot.Body : BodySlot.Unknown), }; } From 09742e2e50d24acfe62e4876451f9be556b457fe Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 28 May 2024 10:56:23 +0000 Subject: [PATCH 112/865] [CI] Updating repo.json for testing_1.0.3.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 1cf9c104..a996e2d0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.0.1.0", - "TestingAssemblyVersion": "1.0.3.1", + "TestingAssemblyVersion": "1.0.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b2e1bff782f5aff12766ecbe82bfa743242e0705 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 May 2024 17:18:39 +0200 Subject: [PATCH 113/865] Consolidate path-data encoding into a single file and make it neater. --- Penumbra/Collections/Cache/ImcCache.cs | 16 +- .../Collections/Manager/CollectionStorage.cs | 64 ++++++- .../Manager/TempCollectionManager.cs | 3 +- Penumbra/Collections/ModCollection.cs | 27 +-- .../Interop/PathResolving/PathDataHandler.cs | 162 ++++++++++++++++++ .../Interop/PathResolving/PathResolver.cs | 52 +++--- .../Interop/PathResolving/SubfileHelper.cs | 19 +- .../ResourceLoading/CreateFileWHook.cs | 3 +- .../Interop/ResourceLoading/ResourceLoader.cs | 24 ++- .../Interop/ResourceTree/ResolveContext.cs | 11 +- Penumbra/Mods/TemporaryMod.cs | 7 +- Penumbra/Services/ConfigMigrationService.cs | 3 +- Penumbra/Services/CrashHandlerService.cs | 4 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- 14 files changed, 302 insertions(+), 95 deletions(-) create mode 100644 Penumbra/Interop/PathResolving/PathDataHandler.cs diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 843fe195..33b366d3 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,3 +1,4 @@ +using Penumbra.Interop.PathResolving; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -7,8 +8,8 @@ namespace Penumbra.Collections.Cache; public readonly struct ImcCache : IDisposable { - private readonly Dictionary _imcFiles = new(); - private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = new(); + private readonly Dictionary _imcFiles = []; + private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = []; public ImcCache() { } @@ -17,10 +18,10 @@ public readonly struct ImcCache : IDisposable { if (fromFullCompute) foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFileSync(path, CreateImcPath(collection, path)); + collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection)); else foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFile(path, CreateImcPath(collection, path)); + collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection)); } public void Reset(ModCollection collection) @@ -57,7 +58,7 @@ public readonly struct ImcCache : IDisposable return false; _imcFiles[path] = file; - var fullPath = CreateImcPath(collection, path); + var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); collection._cache!.ForceFile(path, fullPath); return true; @@ -100,7 +101,7 @@ public readonly struct ImcCache : IDisposable if (!manip.Apply(file)) return false; - var fullPath = CreateImcPath(collection, file.Path); + var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); collection._cache!.ForceFile(file.Path, fullPath); return true; @@ -115,9 +116,6 @@ public readonly struct ImcCache : IDisposable _imcManipulations.Clear(); } - private static FullPath CreateImcPath(ModCollection collection, Utf8GamePath path) - => new($"|{collection.Id.OptimizedString()}_{collection.ChangeCounter}|{path}"); - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) => _imcFiles.TryGetValue(path, out file); } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index de5d0a14..39068e87 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -10,23 +11,71 @@ using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; +using Penumbra.UI.CollectionTab; namespace Penumbra.Collections.Manager; +/// A contiguously incrementing ID managed by the CollectionCreator. +public readonly record struct LocalCollectionId(int Id) : IAdditionOperators +{ + public static readonly LocalCollectionId Zero = new(0); + + public static LocalCollectionId operator +(LocalCollectionId left, int right) + => new(left.Id + right); +} + public class CollectionStorage : IReadOnlyList, IDisposable { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; private readonly ModStorage _modStorage; + public ModCollection Create(string name, int index, ModCollection? duplicate) + { + var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index) + ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, + IReadOnlyList inheritances) + { + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings, + inheritances); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public ModCollection CreateTemporary(string name, int index, int globalChangeCounter) + { + var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public void Delete(ModCollection collection) + => _collectionsByLocal.Remove(collection.LocalId); + /// The empty collection is always available at Index 0. private readonly List _collections = [ ModCollection.Empty, ]; + /// A list of all collections ever created still existing by their local id. + private readonly Dictionary + _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty }; + + public readonly ModCollection DefaultNamed; + /// Incremented by 1 because the empty collection gets Zero. + public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1; + /// Default enumeration skips the empty collection. public IEnumerator GetEnumerator() => _collections.Skip(1).GetEnumerator(); @@ -69,6 +118,10 @@ public class CollectionStorage : IReadOnlyList, IDisposable return ByName(identifier, out collection); } + /// Find a collection by its local ID if it still exists, otherwise returns the empty collection. + public ModCollection ByLocalId(LocalCollectionId localId) + => _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty; + public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) { _communicator = communicator; @@ -100,10 +153,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// public bool AddCollection(string name, ModCollection? duplicate) { - var newCollection = duplicate?.Duplicate(name, _collections.Count) - ?? ModCollection.CreateEmpty(name, _collections.Count, _modStorage.Count); - _collections.Add(newCollection); - + var newCollection = Create(name, _collections.Count, duplicate); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); @@ -132,6 +182,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable // Update indices. for (var i = collection.Index; i < Count; ++i) _collections[i].Index = i; + _collectionsByLocal.Remove(collection.LocalId); Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); @@ -180,7 +231,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable continue; } - var collection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, version, Count, settings, inheritance); + var collection = CreateFromData(id, name, version, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) try @@ -293,7 +344,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable } /// Save all collections where the mod has settings and the change requires saving. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int movedToIdx) { type.HandlingInfo(out var requiresSaving, out _, out _); if (!requiresSaving) diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index de08c6a2..ce438a6b 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -52,7 +52,7 @@ public class TempCollectionManager : IDisposable { if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; - var collection = ModCollection.CreateTemporary(name, ~Count, GlobalChangeCounter++); + var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++); Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}."); if (_customCollections.TryAdd(collection.Id, collection)) { @@ -72,6 +72,7 @@ public class TempCollectionManager : IDisposable return false; } + _storage.Delete(collection); Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 4580e37a..9286d459 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -25,13 +25,15 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, 0, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []); /// The name of a collection. public string Name { get; set; } public Guid Id { get; } + public LocalCollectionId LocalId { get; } + public string Identifier => Id.ToString(); @@ -117,19 +119,20 @@ public partial class ModCollection /// /// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists. /// - public ModCollection Duplicate(string name, int index) + public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), + return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. - public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, int version, int index, + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version, + int index, Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(id, name, index, 0, version, [], [], allSettings) + var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings) { InheritanceByName = inheritances, }; @@ -139,18 +142,19 @@ public partial class ModCollection } /// Constructor for temporary collections. - public static ModCollection CreateTemporary(string name, int index, int changeCounter) + public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(Guid.NewGuid(), name, index, changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []); return ret; } /// Constructor for empty collections. - public static ModCollection CreateEmpty(string name, int index, int modCount) + public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(Guid.NewGuid(), name, index, 0, CurrentVersion, Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], + return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, + Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); } @@ -199,11 +203,12 @@ public partial class ModCollection saver.ImmediateSave(new ModCollectionSave(mods, this)); } - private ModCollection(Guid id, string name, int index, int changeCounter, int version, List appliedSettings, - List inheritsFrom, Dictionary settings) + private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version, + List appliedSettings, List inheritsFrom, Dictionary settings) { Name = name; Id = id; + LocalId = localId; Index = index; ChangeCounter = changeCounter; Settings = appliedSettings; diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs new file mode 100644 index 00000000..5627e015 --- /dev/null +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -0,0 +1,162 @@ +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.PathResolving; + +public static class PathDataHandler +{ + public static readonly ushort Discriminator = (ushort)(Environment.TickCount >> 12); + private static readonly string DiscriminatorString = $"{Discriminator:X4}"; + private const int MinimumLength = 8; + + /// Additional Data encoded in a path. + /// The local ID of the collection. + /// The change counter of that collection when this file was loaded. + /// The CRC32 of the originally requested path, only used for materials. + /// A discriminator to differ between multiple loads of Penumbra. + public readonly record struct AdditionalPathData( + LocalCollectionId Collection, + int ChangeCounter, + int OriginalPathCrc32, + ushort Discriminator) + { + public static readonly AdditionalPathData Invalid = new(LocalCollectionId.Zero, 0, 0, PathDataHandler.Discriminator); + + /// Any collection but the empty collection can appear. In particular, they can be negative for temporary collections. + public bool Valid + => Collection.Id != 0; + } + + /// Create the encoding path for an IMC file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateImc(ByteString path, ModCollection collection) + => CreateBase(path, collection); + + /// Create the encoding path for a TMB file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateTmb(ByteString path, ModCollection collection) + => CreateBase(path, collection); + + /// Create the encoding path for an AVFX file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateAvfx(ByteString path, ModCollection collection) + => CreateBase(path, collection); + + /// Create the encoding path for a MTRL file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateMtrl(ByteString path, ModCollection collection, Utf8GamePath originalPath) + => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); + + /// The base function shared by most file types. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static FullPath CreateBase(ByteString path, ModCollection collection) + => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}"); + + /// Read an additional data blurb and parse it into usable data for all file types but Materials. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Read(ReadOnlySpan additionalData, out AdditionalPathData data) + => ReadBase(additionalData, out data, out _); + + /// Read an additional data blurb and parse it into usable data for Materials. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadMtrl(ReadOnlySpan additionalData, out AdditionalPathData data) + { + if (!ReadBase(additionalData, out data, out var remaining)) + return false; + + if (!int.TryParse(remaining, out var crc32)) + return false; + + data = data with { OriginalPathCrc32 = crc32 }; + return true; + } + + /// Parse the common attributes of an additional data blurb and return remaining data if there is any. + private static bool ReadBase(ReadOnlySpan additionalData, out AdditionalPathData data, out ReadOnlySpan remainingData) + { + data = AdditionalPathData.Invalid; + remainingData = []; + + // At least (\d_\d_\x\x\x\x) + if (additionalData.Length < MinimumLength) + return false; + + // Fetch discriminator, constant length. + var discriminatorSpan = additionalData[^4..]; + if (!ushort.TryParse(discriminatorSpan, NumberStyles.HexNumber, CultureInfo.CurrentCulture, out var discriminator)) + return false; + + additionalData = additionalData[..^5]; + var collectionSplit = additionalData.IndexOf((byte)'_'); + if (collectionSplit == -1) + return false; + + var collectionSpan = additionalData[..collectionSplit]; + additionalData = additionalData[(collectionSplit + 1)..]; + + if (!int.TryParse(collectionSpan, out var id)) + return false; + + var changeCounterSpan = additionalData; + var changeCounterSplit = additionalData.IndexOf((byte)'_'); + if (changeCounterSplit != -1) + { + changeCounterSpan = additionalData[..changeCounterSplit]; + remainingData = additionalData[(changeCounterSplit + 1)..]; + } + + if (!int.TryParse(changeCounterSpan, out var changeCounter)) + return false; + + data = new AdditionalPathData(new LocalCollectionId(id), changeCounter, 0, discriminator); + return true; + } + + /// Split a given span into the actual path and the additional data blurb. Returns true if a blurb exists. + public static bool Split(ReadOnlySpan text, out ReadOnlySpan path, out ReadOnlySpan data) + { + if (text.IsEmpty || text[0] is not (byte)'|') + { + path = text; + data = []; + return false; + } + + var endIdx = text[1..].IndexOf((byte)'|'); + if (endIdx++ < 0) + { + path = text; + data = []; + return false; + } + + data = text.Slice(1, endIdx - 1); + path = ++endIdx == text.Length ? [] : text[endIdx..]; + return true; + } + + /// + public static bool Split(ReadOnlySpan text, out ReadOnlySpan path, out ReadOnlySpan data) + { + if (text.Length == 0 || text[0] is not '|') + { + path = text; + data = []; + return false; + } + + var endIdx = text[1..].IndexOf('|'); + if (endIdx++ < 0) + { + path = text; + data = []; + return false; + } + + data = text.Slice(1, endIdx - 1); + path = ++endIdx >= text.Length ? [] : text[endIdx..]; + return true; + } +} diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 5c3d8d19..e5c75327 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -13,11 +13,10 @@ namespace Penumbra.Interop.PathResolving; public class PathResolver : IDisposable { - private readonly PerformanceTracker _performance; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly TempCollectionManager _tempCollections; - private readonly ResourceLoader _loader; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + private readonly ResourceLoader _loader; private readonly SubfileHelper _subfileHelper; private readonly PathState _pathState; @@ -25,14 +24,12 @@ public class PathResolver : IDisposable private readonly GameState _gameState; private readonly CollectionResolver _collectionResolver; - public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, - TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper, - PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) + public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, + SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) { _performance = performance; _config = config; _collectionManager = collectionManager; - _tempCollections = tempCollections; _subfileHelper = subfileHelper; _pathState = pathState; _metaState = metaState; @@ -43,9 +40,12 @@ public class PathResolver : IDisposable _loader.FileLoaded += ImcLoadResource; } - /// Obtain a temporary or permanent collection by name. - public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionById(id, out collection) || _collectionManager.Storage.ById(id, out collection); + /// Obtain a temporary or permanent collection by local ID. + public bool CollectionByLocalId(LocalCollectionId id, out ModCollection collection) + { + collection = _collectionManager.Storage.ByLocalId(id); + return collection != ModCollection.Empty; + } /// Try to resolve the given game path to the replaced path. public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) @@ -113,7 +113,7 @@ public class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair); + SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, gamePath, out var pair); return pair; } @@ -131,23 +131,21 @@ public class PathResolver : IDisposable } /// After loading an IMC file, replace its contents with the modded IMC file. - private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData) + private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ReadOnlySpan additionalData) { - if (resource->FileType != ResourceType.Imc) + if (resource->FileType != ResourceType.Imc + || !PathDataHandler.Read(additionalData, out var data) + || data.Discriminator != PathDataHandler.Discriminator + || !Utf8GamePath.FromByteString(path, out var gamePath) + || !CollectionByLocalId(data.Collection, out var collection) + || !collection.HasCache + || !collection.GetImcFile(gamePath, out var file)) return; - var lastUnderscore = additionalData.LastIndexOf((byte)'_'); - var idString = lastUnderscore == -1 ? additionalData : additionalData.Substring(0, lastUnderscore); - if (Utf8GamePath.FromByteString(path, out var gamePath) - && GuidExtensions.FromOptimizedString(idString.Span, out var id) - && CollectionById(id, out var collection) - && collection.HasCache - && collection.GetImcFile(gamePath, out var file)) - { - file.Replace(resource); - Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } + file.Replace(resource); + Penumbra.Log.Verbose( + $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); } /// Resolve a path from the interface collection. diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 844baaa9..793ea20b 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -66,21 +66,18 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection Materials, TMB, and AVFX need to be set per collection so they can load their sub files independently from each other. + /// Materials, TMB, and AVFX need to be set per collection, so they can load their sub files independently of each other. public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, - out (FullPath?, ResolveData) data) + Utf8GamePath originalPath, out (FullPath?, ResolveData) data) { if (nonDefault) - switch (type) + resolved = type switch { - case ResourceType.Mtrl: - case ResourceType.Avfx: - case ResourceType.Tmb: - var fullPath = new FullPath($"|{resolveData.ModCollection.Id.OptimizedString()}_{resolveData.ModCollection.ChangeCounter}|{path}"); - data = (fullPath, resolveData); - return; - } - + ResourceType.Mtrl => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), + ResourceType.Avfx => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), + ResourceType.Tmb => PathDataHandler.CreateTmb(path, resolveData.ModCollection), + _ => resolved, + }; data = (resolved, resolveData); } diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs index 8a5e779b..bde640d2 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; +using OtterGui.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -11,7 +12,7 @@ namespace Penumbra.Interop.ResourceLoading; /// we use the fixed size buffers of their formats to only store pointers to the actual path instead. /// Then we translate the stored pointer to the path in CreateFileW, if the prefix matches. /// -public unsafe class CreateFileWHook : IDisposable +public unsafe class CreateFileWHook : IDisposable, IRequiredService { public const int Size = 28; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 6c2c83b3..7b49beab 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; using Penumbra.String; @@ -17,8 +18,7 @@ public unsafe class ResourceLoader : IDisposable private ResolveData _resolvedData = ResolveData.Invalid; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, - CreateFileWHook _) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { _resources = resources; _fileReadService = fileReadService; @@ -54,7 +54,7 @@ public unsafe class ResourceLoader : IDisposable /// Reset the ResolvePath function to always return null. public void ResetResolvePath() - => ResolvePath = (_1, _2, _3) => (null, ResolveData.Invalid); + => ResolvePath = (_, _, _) => (null, ResolveData.Invalid); public delegate void ResourceLoadedDelegate(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData); @@ -67,7 +67,7 @@ public unsafe class ResourceLoader : IDisposable public event ResourceLoadedDelegate? ResourceLoaded; public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ByteString additionalData); + ReadOnlySpan additionalData); /// /// Event fired whenever a resource is newly loaded. @@ -132,19 +132,17 @@ public unsafe class ResourceLoader : IDisposable // Paths starting with a '|' are handled separately to allow for special treatment. // They are expected to also have a closing '|'. - if (gamePath.Path[0] != (byte)'|') + if (!PathDataHandler.Split(gamePath.Path.Span, out var actualPath, out var data)) { - returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, ByteString.Empty); + returnValue = DefaultLoadResource(gamePath.Path, fileDescriptor, priority, isSync, []); return; } - // Split the path into the special-treatment part (between the first and second '|') - // and the actual path. - var split = gamePath.Path.Split((byte)'|', 3, false); - fileDescriptor->ResourceHandle->FileNameData = split[2].Path; - fileDescriptor->ResourceHandle->FileNameLength = split[2].Length; + var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); + fileDescriptor->ResourceHandle->FileNameData = path.Path; + fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); - returnValue = DefaultLoadResource(split[2], fileDescriptor, priority, isSync, split[1]); + returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; @@ -153,7 +151,7 @@ public unsafe class ResourceLoader : IDisposable /// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, - bool isSync, ByteString additionalData) + bool isSync, ReadOnlySpan additionalData) { if (Utf8GamePath.IsRooted(gamePath)) { diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 615ef2b0..7c8da41f 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -9,6 +9,7 @@ using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.Services; using Penumbra.String; using Penumbra.String.Classes; @@ -373,14 +374,8 @@ internal unsafe partial record ResolveContext( if (name.IsEmpty) return ByteString.Empty; - if (stripPrefix && name[0] == (byte)'|') - { - var pos = name.IndexOf((byte)'|', 1); - if (pos < 0) - return ByteString.Empty; - - name = name.Substring(pos + 1); - } + if (stripPrefix && PathDataHandler.Split(name.Span, out var path, out _)) + name = ByteString.FromSpanUnsafe(path, name.IsNullTerminated, name.IsAsciiLowerCase, name.IsAscii); return name; } diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index d08c8b06..6e6e72ab 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Collections; +using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -83,10 +84,8 @@ public class TemporaryMod : IMod } var targetPath = fullPath.Path.FullName; - if (fullPath.Path.Name.StartsWith('|')) - { - targetPath = targetPath.Split('|', 3, StringSplitOptions.RemoveEmptyEntries).Last(); - } + if (PathDataHandler.Split(fullPath.Path.FullName, out var actualPath, out _)) + targetPath = actualPath.ToString(); if (Path.IsPathRooted(targetPath)) { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 1f6ac170..70b05a73 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -379,7 +379,8 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); - var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, 0, 1, dict, []); + // Only used for saving and immediately discarded, so the local collection id here is irrelevant. + var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, LocalCollectionId.Zero, 0, 1, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 6805e7db..1239578b 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -7,6 +7,7 @@ using Penumbra.Communication; using Penumbra.CrashHandler; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData.Actors; +using Penumbra.Interop.PathResolving; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; @@ -286,8 +287,7 @@ public sealed class CrashHandlerService : IDisposable, IService try { - var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1; - if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1))) + if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && Path.IsPathRooted(actualPath)) return; var name = GetActorName(resolveData.AssociatedGameObject); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index d5ff1abd..65a8fe76 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -236,7 +236,7 @@ public sealed class ResourceWatcher : IDisposable, ITab _newRecords.Enqueue(record); } - private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ByteString _) + private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) Penumbra.Log.Information( From a6661f15e87ad8b8b15d386943d08e20001c9848 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 30 May 2024 20:46:04 +0200 Subject: [PATCH 114/865] Display the additional path data in ResourceTree --- .../Interop/MaterialPreview/MaterialInfo.cs | 11 +++++--- .../ResolveContext.PathResolution.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 27 +++++++------------ Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 +++ .../UI/AdvancedWindow/ResourceTreeViewer.cs | 8 ++++-- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index 61e7c764..f7e6caf0 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -3,8 +3,9 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; -using Penumbra.Interop.ResourceTree; +using Penumbra.Interop.PathResolving; using Penumbra.String; +using static Penumbra.Interop.Structs.StructExtensions; using Model = Penumbra.GameData.Interop.Model; namespace Penumbra.Interop.MaterialPreview; @@ -78,8 +79,12 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy continue; var mtrlHandle = material->MaterialResourceHandle; - var path = ResolveContext.GetResourceHandlePath(&mtrlHandle->ResourceHandle); - if (path == needle) + if (mtrlHandle == null) + continue; + + PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); + var fileName = ByteString.FromSpanUnsafe(path, true); + if (fileName == needle) result.Add(new MaterialInfo(index, type, i, j)); } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index d5b4fa39..236c7051 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -155,7 +155,7 @@ internal partial record ResolveContext var imcFileData = imc->GetDataSpan(); if (imcFileData.IsEmpty) { - Penumbra.Log.Warning($"IMC resource handle with path {GetResourceHandlePath(imc, false)} doesn't have a valid data span"); + Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span"); return variant.Id; } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 7c8da41f..e38bf4f6 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -111,12 +111,18 @@ internal unsafe partial record ResolveContext( if (resourceHandle == null) throw new ArgumentNullException(nameof(resourceHandle)); - var fullPath = Utf8GamePath.FromByteString(GetResourceHandlePath(resourceHandle), out var p) ? new FullPath(p) : FullPath.Empty; + var fileName = resourceHandle->FileName.AsSpan(); + var additionalData = ByteString.Empty; + if (PathDataHandler.Split(fileName, out fileName, out var data)) + additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); + + var fullPath = Utf8GamePath.FromSpan(fileName, out var p) ? new FullPath(p.Clone()) : FullPath.Empty; var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this) { - GamePath = gamePath, - FullPath = fullPath, + GamePath = gamePath, + FullPath = fullPath, + AdditionalData = additionalData, }; if (autoAdd) Global.Nodes.Add((gamePath, (nint)resourceHandle), node); @@ -365,21 +371,6 @@ internal unsafe partial record ResolveContext( return i >= 0 && i < array.Length ? array[i] : null; } - internal static ByteString GetResourceHandlePath(ResourceHandle* handle, bool stripPrefix = true) - { - if (handle == null) - return ByteString.Empty; - - var name = handle->FileName.AsByteString(); - if (name.IsEmpty) - return ByteString.Empty; - - if (stripPrefix && PathDataHandler.Split(name.Span, out var path, out _)) - name = ByteString.FromSpanUnsafe(path, name.IsNullTerminated, name.IsAsciiLowerCase, name.IsAscii); - - return name; - } - private static ulong GetResourceHandleLength(ResourceHandle* handle) { if (handle == null) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 7ec75893..e74edb91 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,4 +1,5 @@ using Penumbra.Api.Enums; +using Penumbra.String; using Penumbra.String.Classes; using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; @@ -15,6 +16,7 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; + public ByteString AdditionalData; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; @@ -40,6 +42,7 @@ public class ResourceNode : ICloneable ObjectAddress = objectAddress; ResourceHandle = resourceHandle; PossibleGamePaths = Array.Empty(); + AdditionalData = ByteString.Empty; Length = length; Children = new List(); ResolveContext = resolveContext; @@ -56,6 +59,7 @@ public class ResourceNode : ICloneable ResourceHandle = other.ResourceHandle; PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; + AdditionalData = other.AdditionalData; Length = other.Length; Children = other.Children; ResolveContext = other.ResolveContext; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index d31f3e52..3ada77df 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; +using Penumbra.String; namespace Penumbra.UI.AdvancedWindow; @@ -177,6 +178,9 @@ public class ResourceTreeViewer return NodeVisibility.Hidden; } + string GetAdditionalDataSuffix(ByteString data) + => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; + foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { var visibility = GetNodeVisibility(resourceNode); @@ -260,13 +264,13 @@ public class ResourceTreeViewer ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); - ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard."); + ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } else { ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); - ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in."); + ImGuiUtil.HoverTooltip($"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); From f4bdbcac5338f6bb7deb5e021ca2e4d6236de739 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 30 May 2024 23:09:26 +0200 Subject: [PATCH 115/865] Make Resource Trees honor Incognito Mode --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 26 ++++++----- .../ResourceTree/ResourceTreeFactory.cs | 43 ++++++++++--------- Penumbra/Services/StaticServiceManager.cs | 2 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 +-- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 17 +++++--- .../ResourceTreeViewerFactory.cs | 13 ++++++ Penumbra/UI/ChangedItemDrawer.cs | 7 ++- Penumbra/UI/CollectionTab/CollectionPanel.cs | 11 +++-- .../UI/CollectionTab/CollectionSelector.cs | 8 ++-- Penumbra/UI/CollectionTab/InheritanceUi.cs | 4 +- Penumbra/UI/IncognitoService.cs | 29 +++++++++++++ Penumbra/UI/Tabs/CollectionsTab.cs | 25 ++++------- Penumbra/UI/Tabs/OnScreenTab.cs | 7 +-- 13 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs create mode 100644 Penumbra/UI/IncognitoService.cs diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index dac86e44..fc8c805a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -15,6 +15,7 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTree { public readonly string Name; + public readonly string AnonymizedName; public readonly int GameObjectIndex; public readonly nint GameObjectAddress; public readonly nint DrawObjectAddress; @@ -22,6 +23,7 @@ public class ResourceTree public readonly bool PlayerRelated; public readonly bool Networked; public readonly string CollectionName; + public readonly string AnonymizedCollectionName; public readonly List Nodes; public readonly HashSet FlatNodes; @@ -29,18 +31,20 @@ public class ResourceTree public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName) + public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) { - Name = name; - GameObjectIndex = gameObjectIndex; - GameObjectAddress = gameObjectAddress; - DrawObjectAddress = drawObjectAddress; - LocalPlayerRelated = localPlayerRelated; - Networked = networked; - PlayerRelated = playerRelated; - CollectionName = collectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Name = name; + AnonymizedName = anonymizedName; + GameObjectIndex = gameObjectIndex; + GameObjectAddress = gameObjectAddress; + DrawObjectAddress = drawObjectAddress; + LocalPlayerRelated = localPlayerRelated; + Networked = networked; + PlayerRelated = playerRelated; + CollectionName = collectionName; + AnonymizedCollectionName = anonymizedCollectionName; + Nodes = new List(); + FlatNodes = new HashSet(); } public void ProcessPostfix(Action action) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index ae7187f0..a722e344 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -72,10 +72,10 @@ public class ResourceTreeFactory( return null; var localPlayerRelated = cache.IsLocalPlayerRelated(character); - var (name, related) = GetCharacterName(character, cache); + var (name, anonymizedName, related) = GetCharacterName(character); var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; - var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, - networked, collectionResolveData.ModCollection.Name); + var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, + networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) @@ -157,27 +157,30 @@ public class ResourceTreeFactory( } } - private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character, - TreeBuildCache cache) + private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character) { var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); - switch (identifier.Type) - { - case IdentifierType.Player: return (identifier.PlayerName.ToString(), true); - case IdentifierType.Owned: - var ownerChara = objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character; - if (ownerChara != null) - { - var ownerName = GetCharacterName(ownerChara, cache); - return ($"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})", ownerName.PlayerRelated); - } - - break; - } - - return ($"{character.Name} ({identifier.Kind.ToName()})", false); + var identifierStr = identifier.ToString(); + return (identifierStr, identifier.Incognito(identifierStr), IsPlayerRelated(identifier, owner)); } + private unsafe bool IsPlayerRelated(Dalamud.Game.ClientState.Objects.Types.Character? character) + { + if (character == null) + return false; + + var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + return IsPlayerRelated(identifier, owner); + } + + private bool IsPlayerRelated(ActorIdentifier identifier, Actor owner) + => identifier.Type switch + { + IdentifierType.Player => true, + IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character), + _ => false, + }; + [Flags] public enum Flags { diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 0c6648ba..146d7ee0 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -149,6 +149,7 @@ public static class StaticServiceManager private static ServiceManager AddInterface(this ServiceManager services) => services.AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -181,6 +182,7 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(p => new Diagnostics(p)); private static ServiceManager AddModEditor(this ServiceManager services) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 6b48a048..af01047b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -585,7 +585,8 @@ public partial class ModEditWindow : Window, IDisposable Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, - ChangedItemDrawer changedItemDrawer, ObjectManager objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor) + ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, + CharacterBaseDestructor characterBaseDestructor) : base(WindowBaseLabel) { _performance = performance; @@ -618,8 +619,7 @@ public partial class ModEditWindow : Window, IDisposable _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; - _quickImportViewer = - new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3ada77df..c0c49e47 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -6,6 +6,7 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; using Penumbra.String; +using Penumbra.UI.Tabs; namespace Penumbra.UI.AdvancedWindow; @@ -17,6 +18,7 @@ public class ResourceTreeViewer private readonly Configuration _config; private readonly ResourceTreeFactory _treeFactory; private readonly ChangedItemDrawer _changedItemDrawer; + private readonly IncognitoService _incognito; private readonly int _actionCapacity; private readonly Action _onRefresh; private readonly Action _drawActions; @@ -29,11 +31,12 @@ public class ResourceTreeViewer private Task? _task; public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - int actionCapacity, Action onRefresh, Action drawActions) + IncognitoService incognito, int actionCapacity, Action onRefresh, Action drawActions) { _config = config; _treeFactory = treeFactory; _changedItemDrawer = changedItemDrawer; + _incognito = incognito; _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; @@ -75,7 +78,7 @@ public class ResourceTreeViewer using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { - var isOpen = ImGui.CollapsingHeader($"{tree.Name}##{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); + var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); @@ -89,7 +92,7 @@ public class ResourceTreeViewer using var id = ImRaii.PushId(index); - ImGui.TextUnformatted($"Collection: {tree.CollectionName}"); + ImGui.TextUnformatted($"Collection: {(_incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); @@ -137,10 +140,14 @@ public class ResourceTreeViewer ImGui.SameLine(0, checkPadding); - _changedItemDrawer.DrawTypeFilter(ref _typeFilter, -yOffset); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); + using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) + _changedItemDrawer.DrawTypeFilter(ref _typeFilter); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - checkSpacing - ImGui.GetFrameHeightWithSpacing()); ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + ImGui.SameLine(0, checkSpacing); + _incognito.DrawToggle(); } private Task RefreshCharacterList() diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs new file mode 100644 index 00000000..91dab6cb --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -0,0 +1,13 @@ +using Penumbra.Interop.ResourceTree; + +namespace Penumbra.UI.AdvancedWindow; + +public class ResourceTreeViewerFactory( + Configuration config, + ResourceTreeFactory treeFactory, + ChangedItemDrawer changedItemDrawer, + IncognitoService incognito) +{ + public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions); +} diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 638afef0..29a1f291 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -212,7 +212,7 @@ public class ChangedItemDrawer : IDisposable return; var typeFilter = _config.Ephemeral.ChangedItemFilter; - if (DrawTypeFilter(ref typeFilter, 0.0f)) + if (DrawTypeFilter(ref typeFilter)) { _config.Ephemeral.ChangedItemFilter = typeFilter; _config.Ephemeral.Save(); @@ -220,7 +220,7 @@ public class ChangedItemDrawer : IDisposable } /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIcon typeFilter, float yOffset) + public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) { var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); @@ -233,7 +233,6 @@ public class ChangedItemDrawer : IDisposable var ret = false; var icon = _icons[type]; var flag = typeFilter.HasFlag(type); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + yOffset); ImGui.Image(icon.ImGuiHandle, size, Vector2.Zero, Vector2.One, flag ? Vector4.One : new Vector4(0.6f, 0.3f, 0.3f, 1f)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { @@ -267,7 +266,7 @@ public class ChangedItemDrawer : IDisposable ImGui.SameLine(); } - ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionMax().X - size.X, ImGui.GetCursorPosY() + yOffset)); + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index bb22e6a7..cb4dbe20 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -31,6 +31,7 @@ public sealed class CollectionPanel : IDisposable private readonly InheritanceUi _inheritanceUi; private readonly ModStorage _mods; private readonly FilenameService _fileNames; + private readonly IncognitoService _incognito; private readonly IFontHandle _nameFont; private static readonly IReadOnlyDictionary Buttons = CreateButtons(); @@ -41,7 +42,8 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames) + CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames, + IncognitoService incognito) { _collections = manager.Storage; _active = manager.Active; @@ -50,8 +52,9 @@ public sealed class CollectionPanel : IDisposable _targets = targets; _mods = mods; _fileNames = fileNames; + _incognito = incognito; _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); - _inheritanceUi = new InheritanceUi(manager, _selector); + _inheritanceUi = new InheritanceUi(manager, incognito); _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); } @@ -415,7 +418,7 @@ public sealed class CollectionPanel : IDisposable /// Respect incognito mode for names of identifiers. private string Name(ActorIdentifier id, string? name) - => _selector.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned + => _incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned ? id.Incognito(name) : name ?? id.ToString(); @@ -423,7 +426,7 @@ public sealed class CollectionPanel : IDisposable private string Name(ModCollection? collection) => collection == null ? "Unassigned" : collection == ModCollection.Empty ? "Use No Mods" : - _selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + _incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) { diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index fac85d4d..24d3f591 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -17,13 +17,12 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl private readonly CollectionStorage _storage; private readonly ActiveCollections _active; private readonly TutorialService _tutorial; + private readonly IncognitoService _incognito; private ModCollection? _dragging; - public bool IncognitoMode; - public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, - TutorialService tutorial) + TutorialService tutorial, IncognitoService incognito) : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) { _config = config; @@ -31,6 +30,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl _storage = storage; _active = active; _tutorial = tutorial; + _incognito = incognito; _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector); // Set items. @@ -109,7 +109,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } private string Name(ModCollection collection) - => IncognitoMode ? collection.AnonymizedName : collection.Name; + => _incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) { diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 2290592d..418fe52c 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -9,7 +9,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public class InheritanceUi(CollectionManager collectionManager, CollectionSelector selector) : IUiService +public class InheritanceUi(CollectionManager collectionManager, IncognitoService incognito) : IUiService { private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; @@ -312,5 +312,5 @@ public class InheritanceUi(CollectionManager collectionManager, CollectionSelect } private string Name(ModCollection collection) - => selector.IncognitoMode ? collection.AnonymizedName : collection.Name; + => incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; } diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs new file mode 100644 index 00000000..d4b1828f --- /dev/null +++ b/Penumbra/UI/IncognitoService.cs @@ -0,0 +1,29 @@ +using Dalamud.Interface.Utility; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using Penumbra.UI.Classes; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public class IncognitoService(TutorialService tutorial) +{ + public bool IncognitoMode; + + public void DrawToggle(float? buttonWidth = null) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()) + .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); + if (ImGuiUtil.DrawDisabledButton( + $"{(IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", + new Vector2(buttonWidth ?? ImGui.GetFrameHeightWithSpacing(), ImGui.GetFrameHeight()), string.Empty, false, true)) + IncognitoMode = !IncognitoMode; + var hovered = ImGui.IsItemHovered(); + tutorial.OpenTutorial(BasicTutorialSteps.Incognito); + color.Pop(2); + if (hovered) + ImGui.SetTooltip(IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); + } +} diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index fe1471b3..1eaece50 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -21,6 +21,7 @@ public sealed class CollectionsTab : IDisposable, ITab private readonly CollectionSelector _selector; private readonly CollectionPanel _panel; private readonly TutorialService _tutorial; + private readonly IncognitoService _incognito; public enum PanelMode { @@ -40,13 +41,14 @@ public sealed class CollectionsTab : IDisposable, ITab } } - public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, + public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames) { - _config = configuration.Ephemeral; - _tutorial = tutorial; - _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames); + _config = configuration.Ephemeral; + _tutorial = tutorial; + _incognito = incognito; + _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames, incognito); } public void Dispose() @@ -116,18 +118,7 @@ public sealed class CollectionsTab : IDisposable, ITab _tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); ImGui.SameLine(); - style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - color.Push(ImGuiCol.Text, ColorId.FolderExpanded.Value()) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); - if (ImGuiUtil.DrawDisabledButton( - $"{(_selector.IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", - buttonSize with { X = withSpacing }, string.Empty, false, true)) - _selector.IncognitoMode = !_selector.IncognitoMode; - var hovered = ImGui.IsItemHovered(); - _tutorial.OpenTutorial(BasicTutorialSteps.Incognito); - color.Pop(2); - if (hovered) - ImGui.SetTooltip(_selector.IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); + _incognito.DrawToggle(withSpacing); } private void DrawPanel() diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 09772d8e..787e07a1 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -1,18 +1,15 @@ using OtterGui.Widgets; -using Penumbra.Interop.ResourceTree; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.Tabs; public class OnScreenTab : ITab { - private readonly Configuration _config; private readonly ResourceTreeViewer _viewer; - public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer) + public OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) { - _config = config; - _viewer = new ResourceTreeViewer(_config, treeFactory, changedItemDrawer, 0, delegate { }, delegate { }); + _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); } public ReadOnlySpan Label From c7046ec0069bef1ee9d98602589740ed0d3a74bb Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 31 May 2024 01:05:05 +0200 Subject: [PATCH 116/865] ResourceTree: Add name/path filter --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 - .../ResourceTree/ResourceTreeFactory.cs | 3 - .../UI/AdvancedWindow/ResourceTreeViewer.cs | 74 +++++++++++++++---- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index e74edb91..9c911791 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -10,7 +10,6 @@ public class ResourceNode : ICloneable public string? Name; public string? FallbackName; public ChangedItemIcon Icon; - public ChangedItemIcon DescendentIcons; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -53,7 +52,6 @@ public class ResourceNode : ICloneable Name = other.Name; FallbackName = other.FallbackName; Icon = other.Icon; - DescendentIcons = other.DescendentIcons; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index a722e344..5a190e52 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -116,9 +116,6 @@ public class ResourceTreeFactory( { if (node.Name == parent?.Name) node.Name = null; - - if (parent != null) - parent.DescendentIcons |= node.Icon | node.DescendentIcons; }); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c0c49e47..5f376b26 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -24,9 +24,12 @@ public class ResourceTreeViewer private readonly Action _drawActions; private readonly HashSet _unfolded; + private readonly Dictionary _filterCache; + private TreeCategory _categoryFilter; private ChangedItemDrawer.ChangedItemIcon _typeFilter; private string _nameFilter; + private string _nodeFilter; private Task? _task; @@ -40,11 +43,14 @@ public class ResourceTreeViewer _actionCapacity = actionCapacity; _onRefresh = onRefresh; _drawActions = drawActions; - _unfolded = new HashSet(); + _unfolded = []; + + _filterCache = []; _categoryFilter = AllCategories; _typeFilter = ChangedItemDrawer.AllFlags; _nameFilter = string.Empty; + _nodeFilter = string.Empty; } public void Draw() @@ -107,7 +113,7 @@ public class ResourceTreeViewer (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31)); + DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); } } } @@ -140,14 +146,22 @@ public class ResourceTreeViewer ImGui.SameLine(0, checkPadding); + var filterChanged = false; ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) - _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + filterChanged |= _changedItemDrawer.DrawTypeFilter(ref _typeFilter); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - checkSpacing - ImGui.GetFrameHeightWithSpacing()); - ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + var fieldWidth = (ImGui.GetContentRegionAvail().X - checkSpacing * 2.0f - ImGui.GetFrameHeightWithSpacing()) / 2.0f; + ImGui.SetNextItemWidth(fieldWidth); + filterChanged |= ImGui.InputTextWithHint("##TreeNameFilter", "Filter by Character/Entity Name...", ref _nameFilter, 128); + ImGui.SameLine(0, checkSpacing); + ImGui.SetNextItemWidth(fieldWidth); + filterChanged |= ImGui.InputTextWithHint("##NodeFilter", "Filter by Item/Part Name or Path...", ref _nodeFilter, 128); ImGui.SameLine(0, checkSpacing); _incognito.DrawToggle(); + + if (filterChanged) + _filterCache.Clear(); } private Task RefreshCharacterList() @@ -161,36 +175,68 @@ public class ResourceTreeViewer } finally { + _filterCache.Clear(); _unfolded.Clear(); _onRefresh(); } }); - private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash) + private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - NodeVisibility GetNodeVisibility(ResourceNode node) + bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon) + { + if (!_typeFilter.HasFlag(filterIcon)) + return false; + + if (_nodeFilter.Length == 0) + return true; + + return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + } + + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) { if (node.Internal && !debugMode) return NodeVisibility.Hidden; - if (_typeFilter.HasFlag(node.Icon)) + var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon; + if (MatchesFilter(node, filterIcon)) return NodeVisibility.Visible; - if ((_typeFilter & node.DescendentIcons) != 0) - return NodeVisibility.DescendentsOnly; + + foreach (var child in node.Children) + { + if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) + return NodeVisibility.DescendentsOnly; + } return NodeVisibility.Hidden; } + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + { + if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) + { + visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); + _filterCache.Add(nodePathHash, visibility); + } + return visibility; + } + string GetAdditionalDataSuffix(ByteString data) => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { - var visibility = GetNodeVisibility(resourceNode); + var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); + + var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon); if (visibility == NodeVisibility.Hidden) continue; @@ -199,14 +245,14 @@ public class ResourceTreeViewer using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); - var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); + var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(child) != NodeVisibility.Hidden); + var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden); var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; if (unfoldable) { @@ -291,7 +337,7 @@ public class ResourceTreeViewer } if (unfolded) - DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31)); + DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); } } From 81fdbf6ccff729daedc003eba80b3969a4f799f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 16:59:51 +0200 Subject: [PATCH 117/865] Small cleanup. --- Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index e8a27a74..b8faadf7 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -321,8 +321,8 @@ public sealed class ModGroupEditDrawer( && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) return; - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !DragDropTarget.CheckPayload(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) + using var target = ImUtf8.DragDropTarget(); + if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) return; if (_dragDropGroup != null && _dragDropOption != null) From 67bb95f6e64232480ced89d33318edc976206e63 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 17:00:40 +0200 Subject: [PATCH 118/865] Update submodules. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 1d936516..0b5afffd 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1d9365164655a7cb38172e1311e15e19b1def6db +Subproject commit 0b5afffda19d3e16aec9e8682d18c8f11f67f1c6 diff --git a/Penumbra.GameData b/Penumbra.GameData index f2cea65b..29b71cf7 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f2cea65b83b2d6cb0d03339e8f76aed8102a41d5 +Subproject commit 29b71cf7b3cc68995d38f0954fa38c4b9500a81d From f61bd8bb8a5cd2bb45156803d1eddd8b8d74b8f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 19:37:24 +0200 Subject: [PATCH 119/865] Update Changelog and improve metamanipulation display in advanced editing. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 58 +++++++------------ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 30 ++++++---- Penumbra/UI/Changelog.cs | 47 ++++++++------- 3 files changed, 64 insertions(+), 71 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 829161f5..86d5e02e 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -14,13 +15,19 @@ public class ModMetaEditor(ModManager modManager) private readonly HashSet _rsp = []; private readonly HashSet _globalEqp = []; - public int OtherImcCount { get; private set; } - public int OtherEqpCount { get; private set; } - public int OtherEqdpCount { get; private set; } - public int OtherGmpCount { get; private set; } - public int OtherEstCount { get; private set; } - public int OtherRspCount { get; private set; } - public int OtherGlobalEqpCount { get; private set; } + public sealed class OtherOptionData : List + { + public int TotalCount; + + public new void Clear() + { + TotalCount = 0; + base.Clear(); + } + } + + public readonly FrozenDictionary OtherData = + Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); public bool Changes { get; private set; } @@ -114,13 +121,9 @@ public class ModMetaEditor(ModManager modManager) public void Load(Mod mod, IModDataContainer currentOption) { - OtherImcCount = 0; - OtherEqpCount = 0; - OtherEqdpCount = 0; - OtherGmpCount = 0; - OtherEstCount = 0; - OtherRspCount = 0; - OtherGlobalEqpCount = 0; + foreach (var type in Enum.GetValues()) + OtherData[type].Clear(); + foreach (var option in mod.AllDataContainers) { if (option == currentOption) @@ -128,30 +131,9 @@ public class ModMetaEditor(ModManager modManager) foreach (var manip in option.Manipulations) { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - ++OtherImcCount; - break; - case MetaManipulation.Type.Eqdp: - ++OtherEqdpCount; - break; - case MetaManipulation.Type.Eqp: - ++OtherEqpCount; - break; - case MetaManipulation.Type.Est: - ++OtherEstCount; - break; - case MetaManipulation.Type.Gmp: - ++OtherGmpCount; - break; - case MetaManipulation.Type.Rsp: - ++OtherRspCount; - break; - case MetaManipulation.Type.GlobalEqp: - ++OtherGlobalEqpCount; - break; - } + var data = OtherData[manip.ManipulationType]; + ++data.TotalCount; + data.Add(option.GetFullName()); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 6f542377..a2a6925a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -66,37 +66,45 @@ public partial class ModEditWindow return; DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew, - _editor.MetaEditor.OtherEqpCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqp]); DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, - _editor.MetaEditor.OtherEqdpCount); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, _editor.MetaEditor.OtherImcCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqdp]); + DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, + _editor.MetaEditor.OtherData[MetaManipulation.Type.Imc]); DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew, - _editor.MetaEditor.OtherEstCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Est]); DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew, - _editor.MetaEditor.OtherGmpCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Gmp]); DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, - _editor.MetaEditor.OtherRspCount); + _editor.MetaEditor.OtherData[MetaManipulation.Type.Rsp]); DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, - GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherGlobalEqpCount); + GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherData[MetaManipulation.Type.GlobalEqp]); } /// The headers for the different meta changes all have basically the same structure for different types. private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, - Action draw, - Action drawNew, int otherCount) + Action draw, Action drawNew, + ModMetaEditor.OtherOptionData otherOptionData) { const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; var oldPos = ImGui.GetCursorPosY(); var header = ImGui.CollapsingHeader($"{items.Count} {label}"); var newPos = ImGui.GetCursorPos(); - if (otherCount > 0) + if (otherOptionData.TotalCount > 0) { - var text = $"{otherCount} Edits in other Options"; + var text = $"{otherOptionData.TotalCount} Edits in other Options"; var size = ImGui.CalcTextSize(text).X; ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + foreach (var name in otherOptionData) + ImUtf8.Text(name); + } + ImGui.SetCursorPos(newPos); } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 2b2cfa99..3f5a446a 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -47,19 +47,26 @@ public class PenumbraChangelog Add8_2_0(Changelog); Add8_3_0(Changelog); Add1_0_0_0(Changelog); - Add1_0_2_0(Changelog); - Add1_0_3_0(Changelog); + AddDummy(Changelog); + AddDummy(Changelog); + Add1_1_0_0(Changelog); } #region Changelogs - private static void Add1_0_3_0(Changelog log) - => log.NextVersion("Version 1.0.3.0") + private static void Add1_1_0_0(Changelog log) + => log.NextVersion("Version 1.1.0.0") .RegisterImportant( "This update comes, again, with a lot of very heavy backend changes (collections and groups) and thus may introduce new issues.") + .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") + .RegisterHighlight( + "Added an experimental crash handler that is supposed to write a Penumbra.log file when the game crashes, containing Penumbra-specific information.") + .RegisterEntry("This is disabled by default. It can be enabled in Advanced Settings.", 1) .RegisterHighlight("Collections now have associated GUIDs as identifiers instead of their names, so they can now be renamed.") .RegisterEntry("Migrating those collections may introduce issues, please let me know as soon as possible if you encounter any.", 1) .RegisterEntry("A permanent (non-rolling) backup should be created before the migration in case of any issues.", 1) + .RegisterHighlight( + "Added predefined tags that can be setup in the Settings tab and can be more easily applied or removed from mods. (by DZD)") .RegisterHighlight( "A total rework of how options and groups are handled internally, and introduction of the first new group type, the IMC Group.") .RegisterEntry( @@ -75,9 +82,14 @@ public class PenumbraChangelog .RegisterEntry( "This can be used if something like a jacket or a stole is put onto an accessory to prevent it from being hidden in general.", 1) + .RegisterEntry( + "The first empty option in a single-select option group imported from a TTMP will now keep its location instead of being moved to the first option.") + .RegisterEntry("Further empty options are still removed.", 1) .RegisterHighlight( "Added a field to rename mods directly from the mod selector context menu, instead of moving them in the filesystem.") .RegisterEntry("You can choose which rename field (none, either one or both) to display in the settings.", 1) + .RegisterEntry("Added the characterglass.shpk shader file to special shader treatment to fix issues when replacing it. (By Ny)") + .RegisterEntry("Made it more obvious if a user has not set their root directory yet.") .RegisterEntry( "You can now paste your current clipboard text into the mod selector filter with a simple right-click as long as it is not focused.") .RegisterHighlight( @@ -88,29 +100,17 @@ public class PenumbraChangelog .RegisterEntry("Removed the auto-generated descriptions for newly created groups in Penumbra.") .RegisterEntry( "Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") - .RegisterEntry("Made a lot of further improvements on Model import/export (by ackwell).") + .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") + .RegisterEntry("Hovering over meta manipulations in other options in the advanced editing window now shows a list of those options.") .RegisterEntry("Reworked the API and IPC structure heavily.") + .RegisterImportant("This means some plugins interacting with Penumbra may not work correctly until they update.", 1) .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") + .RegisterEntry("Fixed an issue where reloading a mod did not ensure settings for that mod being correct afterwards.") + .RegisterEntry("Fixed some issues with the file sizes of compressed files.") .RegisterEntry("Fixed an issue with merging and deduplicating mods.") .RegisterEntry("Fixed a crash when scanning for mods without access rights to the folder.") .RegisterEntry( - "Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer."); - - private static void Add1_0_2_0(Changelog log) - => log.NextVersion("Version 1.0.2.0") - .RegisterEntry("Updated to .net8 and XIV 6.58, using some new framework facilities to improve performance and stability.") - .RegisterHighlight( - "Added an experimental crash handler that is supposed to write a Penumbra.log file when the game crashes, containing Penumbra-specific information.") - .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") - .RegisterHighlight( - "Added predefined tags that can be setup in the Settings tab and can be more easily applied or removed from mods. (by DZD)") - .RegisterEntry( - "The first empty option in a single-select option group imported from a TTMP will now keep its location instead of being moved to the first option.") - .RegisterEntry("Further empty options are still removed.", 1) - .RegisterEntry("Made it more obvious if a user has not set their root directory yet.") - .RegisterEntry("Added the characterglass.shpk shader file to special shader treatment to fix issues when replacing it. (By Ny)") - .RegisterEntry("Fixed some issues with the file sizes of compressed files.") - .RegisterEntry("Fixed an issue where reloading a mod did not ensure settings for that mod being correct afterwards.") + "Made plugin conform to Dalamud requirements by adding a punchline and another button to open the menu from the installer.") .RegisterEntry("Added an option to automatically redraw the player character when saving files. (1.0.0.8)") .RegisterEntry("Fixed issue with manipulating mods not triggering some events. (1.0.0.7)") .RegisterEntry("Fixed issue with temporary mods not triggering some events. (1.0.0.6)") @@ -762,6 +762,9 @@ public class PenumbraChangelog #endregion + private static void AddDummy(Changelog log) + => log.NextVersion(string.Empty); + private (int, ChangeLogDisplayType) ConfigData() => (_config.Ephemeral.LastSeenVersion, _config.ChangeLogDisplayType); From ce11bec985770af2f9bfdaf68f8e265823dfa38a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 May 2024 22:55:22 +0200 Subject: [PATCH 120/865] Use strings for global eqp. --- Penumbra/Meta/Manipulations/GlobalEqpType.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs index d57af1d9..1a7396f9 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpType.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -1,5 +1,9 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + namespace Penumbra.Meta.Manipulations; +[JsonConverter(typeof(StringEnumConverter))] public enum GlobalEqpType { DoNotHideEarrings, From b79600ea1489dab7cb3dc7dc5e2c28708de6ea3f Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Jun 2024 09:54:26 +0000 Subject: [PATCH 121/865] [CI] Updating repo.json for 1.1.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a996e2d0..54e6b8b4 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.0.1.0", - "TestingAssemblyVersion": "1.0.3.2", + "AssemblyVersion": "1.1.0.0", + "TestingAssemblyVersion": "1.1.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.0.3.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.0.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 24d4e9fac6b15dd700a01cc6ec05b224d508b7f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 18:13:46 +0200 Subject: [PATCH 122/865] Fix collections not being added on creation. --- Penumbra/Collections/Manager/CollectionStorage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 39068e87..68bd08cb 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -154,6 +154,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable public bool AddCollection(string name, ModCollection? duplicate) { var newCollection = Create(name, _collections.Count, duplicate); + _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); From 331b7fbc1de13181aba85099518a14665ec0130e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 18:14:08 +0200 Subject: [PATCH 123/865] Fix other options displaying the same option multiple times. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 86d5e02e..86853755 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -15,7 +15,7 @@ public class ModMetaEditor(ModManager modManager) private readonly HashSet _rsp = []; private readonly HashSet _globalEqp = []; - public sealed class OtherOptionData : List + public sealed class OtherOptionData : HashSet { public int TotalCount; From aba68cfb925a0dc97dd9fa6cc5be3f0fe8310f7a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Jun 2024 16:18:13 +0000 Subject: [PATCH 124/865] [CI] Updating repo.json for 1.1.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 54e6b8b4..3f5d7262 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.1.0.0", - "TestingAssemblyVersion": "1.1.0.0", + "AssemblyVersion": "1.1.0.1", + "TestingAssemblyVersion": "1.1.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5101b73fdcb9db2e4cd992d70b0d74bf66316616 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 20:28:43 +0200 Subject: [PATCH 125/865] Fix issue with creating unnamed collections. --- Penumbra/Collections/Manager/CollectionStorage.cs | 3 +++ Penumbra/UI/CollectionTab/CollectionSelector.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 68bd08cb..f6287320 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -153,6 +153,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable /// public bool AddCollection(string name, ModCollection? duplicate) { + if (name.Length == 0) + return false; + var newCollection = Create(name, _collections.Count, duplicate); _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index fac85d4d..cecb41d7 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -109,7 +109,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } private string Name(ModCollection collection) - => IncognitoMode ? collection.AnonymizedName : collection.Name; + => IncognitoMode || collection.Name.Length == 0 ? collection.AnonymizedName : collection.Name; private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) { From e7cf9d35c9ca7208423c95bc59fbfcddaa415df9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 23:33:21 +0200 Subject: [PATCH 126/865] Add GetChangedItems for Mods. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 9 +++-- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 1 + Penumbra/Api/IpcTester/ModsIpcTester.cs | 44 +++++++++++++++++++------ 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 69d106b4..f1e4e520 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0 +Subproject commit f1e4e520daaa8f23e5c8b71d55e5992b8f6768e2 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index c1e0c684..16dd8be9 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -106,8 +106,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable var fullPath = leaf.FullName(); var isDefault = ModFileSystem.ModHasDefaultPath(mod, fullPath); - var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name); - return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault ); + var isNameDefault = isDefault || ModFileSystem.ModHasDefaultPath(mod, leaf.Name); + return (PenumbraApiEc.Success, fullPath, !isDefault, !isNameDefault); } public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath) @@ -129,4 +129,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable return PenumbraApiEc.PathRenameFailed; } } + + public Dictionary GetChangedItems(string modDirectory, string modName) + => _modManager.TryGetMod(modDirectory, modName, out var mod) + ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + : []; } diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 1d5b1537..0400c694 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 0); + => (5, 1); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index ebf71176..6b146c39 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -49,6 +49,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ModMoved.Provider(pi, api.Mods), IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), + IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index 43f397e5..2be51a80 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; @@ -13,16 +14,17 @@ public class ModsIpcTester : IUiService, IDisposable { private readonly DalamudPluginInterface _pi; - private string _modDirectory = string.Empty; - private string _modName = string.Empty; - private string _pathInput = string.Empty; - private string _newInstallPath = string.Empty; - private PenumbraApiEc _lastReloadEc; - private PenumbraApiEc _lastAddEc; - private PenumbraApiEc _lastDeleteEc; - private PenumbraApiEc _lastSetPathEc; - private PenumbraApiEc _lastInstallEc; - private Dictionary _mods = []; + private string _modDirectory = string.Empty; + private string _modName = string.Empty; + private string _pathInput = string.Empty; + private string _newInstallPath = string.Empty; + private PenumbraApiEc _lastReloadEc; + private PenumbraApiEc _lastAddEc; + private PenumbraApiEc _lastDeleteEc; + private PenumbraApiEc _lastSetPathEc; + private PenumbraApiEc _lastInstallEc; + private Dictionary _mods = []; + private Dictionary _changedItems = []; public readonly EventSubscriber DeleteSubscriber; public readonly EventSubscriber AddSubscriber; @@ -120,6 +122,14 @@ public class ModsIpcTester : IUiService, IDisposable ImGui.SameLine(); ImGui.TextUnformatted(_lastDeleteEc.ToString()); + IpcTester.DrawIntro(GetChangedItems.Label, "Get Changed Items"); + DrawChangedItemsPopup(); + if (ImUtf8.Button("Get##ChangedItems"u8)) + { + _changedItems = new GetChangedItems(_pi).Invoke(_modDirectory, _modName); + ImUtf8.OpenPopup("ChangedItems"u8); + } + IpcTester.DrawIntro(GetModPath.Label, "Current Path"); var (ec, path, def, nameDef) = new GetModPath(_pi).Invoke(_modDirectory, _modName); ImGui.TextUnformatted($"{path} ({(def ? "Custom" : "Default")} Path, {(nameDef ? "Custom" : "Default")} Name) [{ec}]"); @@ -157,4 +167,18 @@ public class ModsIpcTester : IUiService, IDisposable if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); } + + private void DrawChangedItemsPopup() + { + ImGui.SetNextWindowSize(ImGuiHelpers.ScaledVector2(500, 500)); + using var p = ImUtf8.Popup("ChangedItems"u8); + if (!p) + return; + + foreach (var (name, data) in _changedItems) + ImUtf8.Text($"{name}: {data}"); + + if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused()) + ImGui.CloseCurrentPopup(); + } } From cfa58ee19672a335592e5d06bad26a71aa58aee6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 23:42:04 +0200 Subject: [PATCH 127/865] Fix global EQP rings checking bracelets instead. --- Penumbra/Meta/Manipulations/GlobalEqpCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs index 48ffb308..26eb1d05 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpCache.cs @@ -44,10 +44,10 @@ public struct GlobalEqpCache : IService if (_doNotHideBracelets.Contains(armor[7].Set)) original |= EqpEntry.BodyShowBracelet | EqpEntry.HandShowBracelet; - if (_doNotHideBracelets.Contains(armor[8].Set)) + if (_doNotHideRingR.Contains(armor[8].Set)) original |= EqpEntry.HandShowRingR; - if (_doNotHideBracelets.Contains(armor[9].Set)) + if (_doNotHideRingL.Contains(armor[9].Set)) original |= EqpEntry.HandShowRingL; return original; } From ef9d81c061c659c0b43643991b15e8be44b068af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Jun 2024 23:42:17 +0200 Subject: [PATCH 128/865] Fix mod merger. --- Penumbra/Mods/Editor/ModMerger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index d6e21076..32a207ff 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -37,7 +37,7 @@ public class ModMerger : IDisposable public readonly HashSet SelectedOptions = []; - public readonly IReadOnlyList Warnings = []; + public readonly IReadOnlyList Warnings = new List(); public Exception? Error { get; private set; } public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, From 2e6473dc096363901c5665f79eb8afb750c2eb7c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Jun 2024 21:44:53 +0000 Subject: [PATCH 129/865] [CI] Updating repo.json for 1.1.0.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 3f5d7262..5e9e5b37 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.1.0.1", - "TestingAssemblyVersion": "1.1.0.1", + "AssemblyVersion": "1.1.0.2", + "TestingAssemblyVersion": "1.1.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 3deda68eeca875ebd6403359f52877ec7d53b55a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 01:03:51 +0200 Subject: [PATCH 130/865] Small updates. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/StaticServiceManager.cs | 2 -- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- .../ResourceTreeViewerFactory.cs | 3 +- Penumbra/UI/IncognitoService.cs | 29 +++++++++---------- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- .../ModsTab/Groups/MultiModGroupEditDrawer.cs | 2 +- .../Groups/SingleModGroupEditDrawer.cs | 2 +- 9 files changed, 21 insertions(+), 25 deletions(-) diff --git a/OtterGui b/OtterGui index 0b5afffd..becacbca 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0b5afffda19d3e16aec9e8682d18c8f11f67f1c6 +Subproject commit becacbca4f35595d16ff40dc9639cfa24be3461f diff --git a/Penumbra.GameData b/Penumbra.GameData index 29b71cf7..fed687b5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 29b71cf7b3cc68995d38f0954fa38c4b9500a81d +Subproject commit fed687b536b7c709484db251b690b8821c5ef403 diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 146d7ee0..0c6648ba 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -149,7 +149,6 @@ public static class StaticServiceManager private static ServiceManager AddInterface(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -182,7 +181,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton(p => new Diagnostics(p)); private static ServiceManager AddModEditor(this ServiceManager services) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 5f376b26..7315f136 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -158,7 +158,7 @@ public class ResourceTreeViewer ImGui.SetNextItemWidth(fieldWidth); filterChanged |= ImGui.InputTextWithHint("##NodeFilter", "Filter by Item/Part Name or Path...", ref _nodeFilter, 128); ImGui.SameLine(0, checkSpacing); - _incognito.DrawToggle(); + _incognito.DrawToggle(ImGui.GetFrameHeightWithSpacing()); if (filterChanged) _filterCache.Clear(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 91dab6cb..ea64c0bf 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Interop.ResourceTree; namespace Penumbra.UI.AdvancedWindow; @@ -6,7 +7,7 @@ public class ResourceTreeViewerFactory( Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - IncognitoService incognito) + IncognitoService incognito) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions); diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index d4b1828f..d58ea1ec 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -1,29 +1,26 @@ -using Dalamud.Interface.Utility; using Dalamud.Interface; -using ImGuiNET; -using OtterGui; using Penumbra.UI.Classes; using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; namespace Penumbra.UI; -public class IncognitoService(TutorialService tutorial) +public class IncognitoService(TutorialService tutorial) : IService { public bool IncognitoMode; - public void DrawToggle(float? buttonWidth = null) + public void DrawToggle(float width) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()) - .Push(ImGuiCol.Border, ColorId.FolderExpanded.Value()); - if (ImGuiUtil.DrawDisabledButton( - $"{(IncognitoMode ? FontAwesomeIcon.Eye : FontAwesomeIcon.EyeSlash).ToIconString()}###IncognitoMode", - new Vector2(buttonWidth ?? ImGui.GetFrameHeightWithSpacing(), ImGui.GetFrameHeight()), string.Empty, false, true)) - IncognitoMode = !IncognitoMode; - var hovered = ImGui.IsItemHovered(); + var color = ColorId.FolderExpanded.Value(); + using (ImRaii.PushFrameBorder(ImUtf8.GlobalScale, color)) + { + var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8; + var icon = IncognitoMode ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; + if (ImUtf8.IconButton(icon, tt, new Vector2(width, ImUtf8.FrameHeight), false, color)) + IncognitoMode = !IncognitoMode; + } + tutorial.OpenTutorial(BasicTutorialSteps.Incognito); - color.Pop(2); - if (hovered) - ImGui.SetTooltip(IncognitoMode ? "Toggle incognito mode off." : "Toggle incognito mode on."); } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index b10f123c..b129d275 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -118,7 +118,7 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr : validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, !validName || dis)) + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, default, !validName || dis)) { editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); editor.NewOptionName = null; diff --git a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs index e6701a03..f0275853 100644 --- a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs @@ -54,7 +54,7 @@ public readonly struct MultiModGroupEditDrawer(ModGroupEditDrawer editor, MultiM var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8, default, !validName)) { editor.ModManager.OptionEditor.MultiEditor.AddOption(group, name); editor.NewOptionName = null; diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index 75fbc63a..be2dbd73 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -59,7 +59,7 @@ public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, Singl var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, !validName)) + : "Please enter a name for the new option."u8, default, !validName)) { editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); editor.NewOptionName = null; From 137b752196154960e2687be65728ae9d60bd913b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 01:53:29 +0200 Subject: [PATCH 131/865] Fix Dye Preview not applying. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fed687b5..ad12ddce 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fed687b536b7c709484db251b690b8821c5ef403 +Subproject commit ad12ddcef38a9ed4e4dd7424d748f41c4b97db10 From 05d010a281de9e494c389bcb34fdad35ba9fe13d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 12:08:49 +0200 Subject: [PATCH 132/865] Add some functionality to allow an IMC group to add apply to all variants. --- Penumbra/Collections/Cache/ImcCache.cs | 2 +- .../Import/TexToolsMeta.Deserialization.cs | 2 +- Penumbra/Import/TexToolsMeta.Export.cs | 2 +- Penumbra/Meta/Files/ImcFile.cs | 21 +++++----- Penumbra/Meta/ImcChecker.cs | 40 ++++++++++++++++++- Penumbra/Meta/Manipulations/Imc.cs | 37 +++++++++-------- Penumbra/Mods/Groups/ImcModGroup.cs | 31 ++++++++++---- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 6 +-- .../Manager/OptionEditor/ImcModGroupEditor.cs | 14 ++++++- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 8 +++- 10 files changed, 117 insertions(+), 46 deletions(-) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 33b366d3..7990122a 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -51,7 +51,7 @@ public readonly struct ImcCache : IDisposable try { if (!_imcFiles.TryGetValue(path, out var file)) - file = new ImcFile(manager, manip); + file = new ImcFile(manager, manip.Identifier); _imcManipulations[idx] = (manip, file); if (!manip.Apply(file)) diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index f062ae25..554cf848 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -110,7 +110,7 @@ public partial class TexToolsMeta var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, new ImcEntry()); - var def = new ImcFile(_metaFileManager, manip); + var def = new ImcFile(_metaFileManager, manip.Identifier); var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 03bdbd90..09bd2c12 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -133,7 +133,7 @@ public partial class TexToolsMeta { case MetaManipulation.Type.Imc: var allManips = manips.ToList(); - var baseFile = new ImcFile(manager, allManips[0].Imc); + var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier); foreach (var manip in allManips) manip.Imc.Apply(baseFile); diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 68d3f5b3..5d704cf8 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -9,20 +9,20 @@ namespace Penumbra.Meta.Files; public class ImcException : Exception { - public readonly ImcManipulation Manipulation; - public readonly string GamePath; + public readonly ImcIdentifier Identifier; + public readonly string GamePath; - public ImcException(ImcManipulation manip, Utf8GamePath path) + public ImcException(ImcIdentifier identifier, Utf8GamePath path) { - Manipulation = manip; - GamePath = path.ToString(); + Identifier = identifier; + GamePath = path.ToString(); } public override string Message => "Could not obtain default Imc File.\n" + " Either the default file does not exist (possibly for offhand files from TexTools) or the installation is corrupted.\n" + $" Game Path: {GamePath}\n" - + $" Manipulation: {Manipulation}"; + + $" Manipulation: {Identifier}"; } public unsafe class ImcFile : MetaBaseFile @@ -142,13 +142,14 @@ public unsafe class ImcFile : MetaBaseFile } } - public ImcFile(MetaFileManager manager, ImcManipulation manip) + public ImcFile(MetaFileManager manager, ImcIdentifier identifier) : base(manager, 0) { - Path = manip.GamePath(); - var file = manager.GameData.GetFile(Path.ToString()); + var path = identifier.GamePathString(); + Path = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; + var file = manager.GameData.GetFile(path); if (file == null) - throw new ImcException(manip, Path); + throw new ImcException(identifier, Path); fixed (byte* ptr = file.Data) { diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 14486e21..650919a3 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -4,11 +4,32 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; -public class ImcChecker(MetaFileManager metaFileManager) +public class ImcChecker { + private static readonly Dictionary VariantCounts = []; + private static MetaFileManager? _dataManager; + + + public static int GetVariantCount(ImcIdentifier identifier) + { + if (VariantCounts.TryGetValue(identifier, out var count)) + return count; + + count = GetFile(identifier)?.Count ?? 0; + VariantCounts[identifier] = count; + return count; + } + public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); private readonly Dictionary _cachedDefaultEntries = new(); + private readonly MetaFileManager _metaFileManager; + + public ImcChecker(MetaFileManager metaFileManager) + { + _metaFileManager = metaFileManager; + _dataManager = metaFileManager; + } public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) { @@ -17,7 +38,7 @@ public class ImcChecker(MetaFileManager metaFileManager) try { - var e = ImcFile.GetDefault(metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); entry = new CachedEntry(e, true, entryExists); } catch (Exception) @@ -33,4 +54,19 @@ public class ImcChecker(MetaFileManager metaFileManager) public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, imcManip.EquipSlot, imcManip.BodySlot), storeCache); + + private static ImcFile? GetFile(ImcIdentifier identifier) + { + if (_dataManager == null) + return null; + + try + { + return new ImcFile(_dataManager, identifier); + } + catch + { + return null; + } + } } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index fef86520..2a2f4c03 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -31,12 +31,16 @@ public readonly record struct ImcIdentifier( => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => AddChangedItems(identifier, changedItems, false); + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { - ObjectType.Equipment or ObjectType.Accessory => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, - Variant, - "a"), + ObjectType.Equipment when allVariants => GamePaths.Equipment.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Equipment => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Accessory when allVariants => GamePaths.Accessory.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Accessory => GamePaths.Accessory.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, "a"), @@ -49,24 +53,19 @@ public readonly record struct ImcIdentifier( identifier.Identify(changedItems, path); } - public Utf8GamePath GamePath() - { - return ObjectType switch + public string GamePathString() + => ObjectType switch { - ObjectType.Accessory => Utf8GamePath.FromString(GamePaths.Accessory.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.Equipment => Utf8GamePath.FromString(GamePaths.Equipment.Imc.Path(PrimaryId), out var p) ? p : Utf8GamePath.Empty, - ObjectType.DemiHuman => Utf8GamePath.FromString(GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Monster => Utf8GamePath.FromString(GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - ObjectType.Weapon => Utf8GamePath.FromString(GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), out var p) - ? p - : Utf8GamePath.Empty, - _ => throw new NotImplementedException(), + ObjectType.Accessory => GamePaths.Accessory.Imc.Path(PrimaryId), + ObjectType.Equipment => GamePaths.Equipment.Imc.Path(PrimaryId), + ObjectType.DemiHuman => GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), + ObjectType.Monster => GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), + ObjectType.Weapon => GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), + _ => string.Empty, }; - } + + public Utf8GamePath GamePath() + => Utf8GamePath.FromString(GamePathString(), out var p) ? p : Utf8GamePath.Empty; public MetaIndex FileIndex() => (MetaIndex)(-1); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e0d70aa6..b336203d 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -6,6 +6,7 @@ using OtterGui.Classes; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -31,6 +32,8 @@ public class ImcModGroup(Mod mod) : IModGroup public ImcIdentifier Identifier; public ImcEntry DefaultEntry; + public bool AllVariants; + public FullPath? FindBestMatch(Utf8GamePath gamePath) => null; @@ -39,7 +42,7 @@ public class ImcModGroup(Mod mod) : IModGroup public bool CanBeDisabled { - get => OptionData.Any(m => m.IsDisableSubMod); + get => _canBeDisabled; set { _canBeDisabled = value; @@ -92,8 +95,8 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcManipulation GetManip(ushort mask) - => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, Identifier.Variant.Id, + public ImcManipulation GetManip(ushort mask, Variant variant) + => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) @@ -102,12 +105,23 @@ public class ImcModGroup(Mod mod) : IModGroup return; var mask = GetCurrentMask(setting); - var imc = GetManip(mask); - manipulations.Add(imc); + if (AllVariants) + { + var count = ImcChecker.GetVariantCount(Identifier); + if (count == 0) + manipulations.Add(GetManip(mask, Identifier.Variant)); + else + for (var i = 0; i <= count; ++i) + manipulations.Add(GetManip(mask, (Variant)i)); + } + else + { + manipulations.Add(GetManip(mask, Identifier.Variant)); + } } public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => Identifier.AddChangedItems(identifier, changedItems); + => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) => new(setting.Value & ((1ul << OptionData.Count) - 1)); @@ -120,6 +134,8 @@ public class ImcModGroup(Mod mod) : IModGroup jObj.WriteTo(jWriter); jWriter.WritePropertyName(nameof(DefaultEntry)); serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName(nameof(AllVariants)); + jWriter.WriteValue(AllVariants); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) @@ -156,6 +172,7 @@ public class ImcModGroup(Mod mod) : IModGroup Description = json[nameof(Description)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, }; if (ret.Name.Length == 0) return null; @@ -210,7 +227,7 @@ public class ImcModGroup(Mod mod) : IModGroup if (idx >= 0) return setting.HasFlag(idx); - Penumbra.Log.Warning($"A IMC Group should be able to be disabled, but does not contain a disable option."); + Penumbra.Log.Warning("A IMC Group should be able to be disabled, but does not contain a disable option."); return false; } diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 5a5181a5..ea4ef7b1 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -51,7 +51,7 @@ public static class EquipmentSwap var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); - var imcFileTo = new ImcFile(manager, imcManip); + var imcFileTo = new ImcFile(manager, imcManip.Identifier); var skipFemale = false; var skipMale = false; var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; @@ -121,7 +121,7 @@ public static class EquipmentSwap { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); - var imcFileTo = new ImcFile(manager, imcManip); + var imcFileTo = new ImcFile(manager, imcManip.Identifier); var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -250,7 +250,7 @@ public static class EquipmentSwap PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry); + var imc = new ImcFile(manager, entry.Identifier); EquipItem[] items; Variant[] variants; if (idFrom == idTo) diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index f9fd532f..4aae45a2 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -2,6 +2,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -14,7 +15,8 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ : ModOptionEditor(communicator, saveService, config), IService { /// Add a new, empty imc group with the given manipulation data. - public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, SaveType saveType = SaveType.ImmediateSync) + public ImcModGroup? AddModGroup(Mod mod, string newName, ImcIdentifier identifier, ImcEntry defaultEntry, + SaveType saveType = SaveType.ImmediateSync) { if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) return null; @@ -78,6 +80,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, option.Mod, option.Group, option, null, -1); } + public void ChangeAllVariants(ImcModGroup group, bool allVariants, SaveType saveType = SaveType.Queue) + { + if (group.AllVariants == allVariants) + return; + + group.AllVariants = allVariants; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) { if (group.CanBeDisabled == canBeDisabled) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index b129d275..d346e05c 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -19,7 +19,13 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr var entry = group.DefaultEntry; var changes = false; - ImUtf8.TextFramed(identifier.ToString(), 0, editor.AvailableWidth, borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X; + ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + ImUtf8.SameLineInner(); + var allVariants = group.AllVariants; + if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) + editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); + ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); using (ImUtf8.Group()) { From b63935e81ed45d562a1a898ba5361f6233516798 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Jun 2024 12:09:05 +0200 Subject: [PATCH 133/865] Fix issue with accessory vfx hook. --- Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 4e24ba39..8118343d 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -182,7 +182,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex <= 4) + if (slotIndex is <= 4 or >= 10) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); var changedEquipData = ((Human*)drawObject)->ChangedEquipData; From 63b3a02e95b00dcbed78f55da4db6ba2ce874d23 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jun 2024 17:45:22 +0200 Subject: [PATCH 134/865] Fix issue with crash handler and collections not saving on rename. --- OtterGui | 2 +- Penumbra.CrashHandler/CrashData.cs | 4 +- .../Collections/Manager/CollectionStorage.cs | 1 + Penumbra/Communication/ModSettingChanged.cs | 2 +- Penumbra/Services/CrashHandlerService.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 68 ++++++++----------- .../UI/CollectionTab/CollectionSelector.cs | 22 +++--- Penumbra/UI/Tabs/CollectionsTab.cs | 8 +-- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 2 +- 9 files changed, 52 insertions(+), 59 deletions(-) diff --git a/OtterGui b/OtterGui index becacbca..5de708b2 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit becacbca4f35595d16ff40dc9639cfa24be3461f +Subproject commit 5de708b27ed45c9cdead71742c7061ad9ce64323 diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs index cdac103f..dd75f46e 100644 --- a/Penumbra.CrashHandler/CrashData.cs +++ b/Penumbra.CrashHandler/CrashData.cs @@ -55,7 +55,7 @@ public class CrashData /// The last vfx function invoked before this crash data was generated. public VfxFuncInvokedEntry? LastVfxFuncInvoked - => LastVfxFuncsInvoked.Count == 0 ? default : LastVfxFuncsInvoked[0]; + => LastVFXFuncsInvoked.Count == 0 ? default : LastVFXFuncsInvoked[0]; /// A collection of the last few characters loaded before this crash data was generated. public List LastCharactersLoaded { get; set; } = []; @@ -64,5 +64,5 @@ public class CrashData public List LastModdedFilesLoaded { get; set; } = []; /// A collection of the last few vfx functions invoked before this crash data was generated. - public List LastVfxFuncsInvoked { get; set; } = []; + public List LastVFXFuncsInvoked { get; set; } = []; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index f6287320..67de3a03 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -181,6 +181,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable return false; } + Delete(collection); _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); _collections.RemoveAt(collection.Index); // Update indices. diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index a7da345b..7fda2f35 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -24,7 +24,7 @@ public sealed class ModSettingChanged() { public enum Priority { - /// + /// Api = int.MinValue, /// diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 1239578b..25c6cf57 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -287,7 +287,7 @@ public sealed class CrashHandlerService : IDisposable, IService try { - if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && Path.IsPathRooted(actualPath)) + if (PathDataHandler.Split(manipulatedPath.Value.FullName, out var actualPath, out _) && !Path.IsPathRooted(actualPath)) return; var name = GetActorName(resolveData.AssociatedGameObject); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cb4dbe20..082b78b8 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -20,19 +20,23 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public sealed class CollectionPanel : IDisposable +public sealed class CollectionPanel( + DalamudPluginInterface pi, + CommunicatorService communicator, + CollectionManager manager, + CollectionSelector selector, + ActorManager actors, + ITargetManager targets, + ModStorage mods, + SaveService saveService, + IncognitoService incognito) + : IDisposable { - private readonly CollectionStorage _collections; - private readonly ActiveCollections _active; - private readonly CollectionSelector _selector; - private readonly ActorManager _actors; - private readonly ITargetManager _targets; - private readonly IndividualAssignmentUi _individualAssignmentUi; - private readonly InheritanceUi _inheritanceUi; - private readonly ModStorage _mods; - private readonly FilenameService _fileNames; - private readonly IncognitoService _incognito; - private readonly IFontHandle _nameFont; + private readonly CollectionStorage _collections = manager.Storage; + private readonly ActiveCollections _active = manager.Active; + private readonly IndividualAssignmentUi _individualAssignmentUi = new(communicator, actors, manager); + private readonly InheritanceUi _inheritanceUi = new(manager, incognito); + private readonly IFontHandle _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); private static readonly IReadOnlyDictionary Buttons = CreateButtons(); private static readonly IReadOnlyList<(CollectionType, bool, bool, string, uint)> AdvancedTree = CreateTree(); @@ -41,23 +45,6 @@ public sealed class CollectionPanel : IDisposable private int _draggedIndividualAssignment = -1; - public CollectionPanel(DalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, - CollectionSelector selector, ActorManager actors, ITargetManager targets, ModStorage mods, FilenameService fileNames, - IncognitoService incognito) - { - _collections = manager.Storage; - _active = manager.Active; - _selector = selector; - _actors = actors; - _targets = targets; - _mods = mods; - _fileNames = fileNames; - _incognito = incognito; - _individualAssignmentUi = new IndividualAssignmentUi(communicator, actors, manager); - _inheritanceUi = new InheritanceUi(manager, incognito); - _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); - } - public void Dispose() { _individualAssignmentUi.Dispose(); @@ -237,17 +224,22 @@ public sealed class CollectionPanel : IDisposable var name = _newName ?? collection.Name; var identifier = collection.Identifier; var width = ImGui.GetContentRegionAvail().X; - var fileName = _fileNames.CollectionFile(collection); + var fileName = saveService.FileNames.CollectionFile(collection); ImGui.SetNextItemWidth(width); if (ImGui.InputText("##name", ref name, 128)) _newName = name; - if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null) + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Name) { collection.Name = _newName; - _newName = null; + saveService.QueueSave(new ModCollectionSave(mods, collection)); + selector.RestoreCollections(); + _newName = null; } else if (ImGui.IsItemDeactivated()) + { _newName = null; + } + using (ImRaii.PushFont(UiBuilder.MonoFont)) { if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) @@ -329,7 +321,7 @@ public sealed class CollectionPanel : IDisposable DrawIndividualDragTarget(text, id); if (!invalid) { - _selector.DragTargetAssignment(type, id); + selector.DragTargetAssignment(type, id); var name = Name(collection); var size = ImGui.CalcTextSize(name); var textPos = ImGui.GetItemRectMax() - size - ImGui.GetStyle().FramePadding; @@ -418,7 +410,7 @@ public sealed class CollectionPanel : IDisposable /// Respect incognito mode for names of identifiers. private string Name(ActorIdentifier id, string? name) - => _incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned + => incognito.IncognitoMode && id.Type is IdentifierType.Player or IdentifierType.Owned ? id.Incognito(name) : name ?? id.ToString(); @@ -426,7 +418,7 @@ public sealed class CollectionPanel : IDisposable private string Name(ModCollection? collection) => collection == null ? "Unassigned" : collection == ModCollection.Empty ? "Use No Mods" : - _incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; + incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) { @@ -445,11 +437,11 @@ public sealed class CollectionPanel : IDisposable } private void DrawCurrentCharacter(Vector2 width) - => DrawIndividualButton("Current Character", width, string.Empty, 'c', _actors.GetCurrentPlayer()); + => DrawIndividualButton("Current Character", width, string.Empty, 'c', actors.GetCurrentPlayer()); private void DrawCurrentTarget(Vector2 width) => DrawIndividualButton("Current Target", width, string.Empty, 't', - _actors.FromObject(_targets.Target, false, true, true)); + actors.FromObject(targets.Target, false, true, true)); private void DrawNewPlayer(Vector2 width) => DrawIndividualButton("New Player", width, _individualAssignmentUi.PlayerTooltip, 'p', @@ -610,7 +602,7 @@ public sealed class CollectionPanel : IDisposable ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - foreach (var (mod, (settings, parent)) in _mods.Select(m => (m, collection[m.Index])) + foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection[m.Index])) .Where(t => t.Item2.Settings != null) .OrderBy(t => t.m.Name)) { diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index c14baf5b..024873bf 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -44,7 +44,9 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl if (idx < 0 || idx >= Items.Count) return false; - return _storage.RemoveCollection(Items[idx]); + // Always return false since we handle the selection update ourselves. + _storage.RemoveCollection(Items[idx]); + return false; } protected override bool DeleteButtonEnabled() @@ -111,6 +113,15 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl private string Name(ModCollection collection) => _incognito.IncognitoMode || collection.Name.Length == 0 ? collection.AnonymizedName : collection.Name; + public void RestoreCollections() + { + Items.Clear(); + foreach (var c in _storage.OrderBy(c => c.Name)) + Items.Add(c); + SetFilterDirty(); + SetCurrent(_active.Current); + } + private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? @new, string _3) { switch (type) @@ -122,14 +133,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl SetFilterDirty(); return; case CollectionType.Inactive: - Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Name)) - Items.Add(c); - - if (old == Current) - ClearCurrentSelection(); - else - TryRestoreCurrent(); + RestoreCollections(); SetFilterDirty(); return; default: diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 1eaece50..fabf7561 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,16 +1,12 @@ using Dalamud.Game.ClientState.Objects; -using Dalamud.Interface; -using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI.Classes; using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; @@ -42,13 +38,13 @@ public sealed class CollectionsTab : IDisposable, ITab } public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, - CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, FilenameService fileNames) + CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService) { _config = configuration.Ephemeral; _tutorial = tutorial; _incognito = incognito; _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, fileNames, incognito); + _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, saveService, incognito); } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 4649e548..94c6cbd6 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -96,7 +96,7 @@ public static class CrashDataExtensions if (!table) return; - ImGuiClip.ClippedDraw(data.LastVfxFuncsInvoked, vfx => + ImGuiClip.ClippedDraw(data.LastVFXFuncsInvoked, vfx => { ImGuiUtil.DrawTableColumn(vfx.Age.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString()); From aeb2db9f5d7f390407e4ab760a55b5eb6a4a53ae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jun 2024 17:46:45 +0200 Subject: [PATCH 135/865] Add tooltip to global eqp condition. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index a2a6925a..d4049bd9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -758,6 +758,7 @@ public partial class ModEditWindow if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) _new = _new with { Condition = newId }; + ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); } public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) From 699ae8e1fb5939c8b7c8afe1614d73ba70d6317d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Jun 2024 17:47:11 +0200 Subject: [PATCH 136/865] Fix issue with collection settings being set to negative value for some reason. --- Penumbra/Mods/Settings/Setting.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Penumbra/Mods/Settings/Setting.cs b/Penumbra/Mods/Settings/Setting.cs index 059cbf51..e8ad103c 100644 --- a/Penumbra/Mods/Settings/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -85,6 +85,24 @@ public readonly record struct Setting(ulong Value) public override Setting ReadJson(JsonReader reader, Type objectType, Setting existingValue, bool hasExistingValue, JsonSerializer serializer) - => new(serializer.Deserialize(reader)); + { + try + { + return new Setting(serializer.Deserialize(reader)); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not deserialize setting {reader.Value} to unsigned long:\n{e}"); + try + { + return new Setting((ulong)serializer.Deserialize(reader)); + } + catch + { + Penumbra.Log.Warning($"Could not deserialize setting {reader.Value} to long:\n{e}"); + return Zero; + } + } + } } } From 87fec7783eb3d416f2ffb5aa3cdc1b2c78acfc75 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 4 Jun 2024 12:10:45 +1000 Subject: [PATCH 137/865] Fix blend weight adjustment getting stuck on near-bounds values --- Penumbra/Import/Models/Import/VertexAttribute.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index a4651776..af401ec1 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -150,6 +150,12 @@ public class VertexAttribute { var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); var closestIndex = Enumerable.Range(0, 4) + .Where(index => { + var byteValue = byteValues[index]; + if (adjustment < 0) return byteValue > 0; + if (adjustment > 0) return byteValue < 255; + return true; + }) .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) .MinBy(x => x.delta) .index; From 48dd4bcadb2a43e0cef94e89022da1fd802cec3a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 4 Jun 2024 15:53:35 +0200 Subject: [PATCH 138/865] Bleh. --- Penumbra/UI/FileDialogService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index e5b0fa19..88c0b00f 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -101,7 +101,7 @@ public class FileDialogService : IDisposable private static string HandleRoot(string path) { - if (path.Length == 2 && path[1] == ':') + if (path is [_, ':']) return path + '\\'; return path; From 03bfbcc3095042bdf7eccadc2494074bfe4e5189 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 5 Jun 2024 10:36:38 +0200 Subject: [PATCH 139/865] Fidx wrong group --- Penumbra/Mods/Editor/ModMerger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 32a207ff..8d47051c 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -105,7 +105,7 @@ public class ModMerger : IDisposable throw new Exception( $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); - foreach (var originalOption in group.DataContainers) + foreach (var originalOption in originalGroup.DataContainers) { var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); if (optionCreated) From ceed8531af0da494a9bd3909835e2f8a245491f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jun 2024 16:49:39 +0200 Subject: [PATCH 140/865] Fix GMP Entry edit. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index d4049bd9..0783dd98 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -640,7 +640,7 @@ public partial class ModEditWindow ImGui.SameLine(); if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkB })); + editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); } } From 2e9f1844546476713634c2343e4db2020544faad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Jun 2024 17:26:25 +0200 Subject: [PATCH 141/865] Introduce Identifiers and strong entry types for each meta manipulation and use them in the manipulations. --- Penumbra/Collections/Cache/EstCache.cs | 38 +++--- Penumbra/Collections/Cache/MetaCache.cs | 4 +- .../Collections/ModCollection.Cache.Access.cs | 2 +- Penumbra/Import/Models/ModelManager.cs | 16 +-- .../Import/TexToolsMeta.Deserialization.cs | 12 +- Penumbra/Import/TexToolsMeta.Export.cs | 12 +- Penumbra/Import/TexToolsMeta.Rgsp.cs | 4 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 8 +- .../ResolveContext.PathResolution.cs | 14 +-- Penumbra/Meta/Files/CmpFile.cs | 11 +- Penumbra/Meta/Files/EstFile.cs | 44 +++---- Penumbra/Meta/Manipulations/Eqdp.cs | 87 ++++++++++++++ .../Meta/Manipulations/EqdpManipulation.cs | 23 ++-- Penumbra/Meta/Manipulations/Eqp.cs | 72 +++++++++++ .../Meta/Manipulations/EqpManipulation.cs | 14 ++- Penumbra/Meta/Manipulations/Est.cs | 113 ++++++++++++++++++ .../Meta/Manipulations/EstManipulation.cs | 36 +++--- Penumbra/Meta/Manipulations/Gmp.cs | 39 ++++++ .../Meta/Manipulations/GmpManipulation.cs | 12 +- Penumbra/Meta/Manipulations/Rsp.cs | 52 ++++++++ .../Meta/Manipulations/RspManipulation.cs | 22 ++-- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 6 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 16 +-- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 6 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 23 ++-- Penumbra/UI/Classes/Combos.cs | 2 +- Penumbra/Util/IdentifierExtensions.cs | 8 +- 27 files changed, 533 insertions(+), 163 deletions(-) create mode 100644 Penumbra/Meta/Manipulations/Eqdp.cs create mode 100644 Penumbra/Meta/Manipulations/Eqp.cs create mode 100644 Penumbra/Meta/Manipulations/Est.cs create mode 100644 Penumbra/Meta/Manipulations/Gmp.cs create mode 100644 Penumbra/Meta/Manipulations/Rsp.cs diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 2552cd4a..3a0b4695 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -48,33 +48,33 @@ public struct EstCache : IDisposable } } - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type) { var (file, idx) = type switch { - EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst), - EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst), - EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst), - EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst), + EstType.Face => (_estFaceFile, MetaIndex.FaceEst), + EstType.Hair => (_estHairFile, MetaIndex.HairEst), + EstType.Body => (_estBodyFile, MetaIndex.BodyEst), + EstType.Head => (_estHeadFile, MetaIndex.HeadEst), _ => (null, 0), }; return manager.TemporarilySetFile(file, idx); } - private readonly EstFile? GetEstFile(EstManipulation.EstType type) + private readonly EstFile? GetEstFile(EstType type) { return type switch { - EstManipulation.EstType.Face => _estFaceFile, - EstManipulation.EstType.Hair => _estHairFile, - EstManipulation.EstType.Body => _estBodyFile, - EstManipulation.EstType.Head => _estHeadFile, + EstType.Face => _estFaceFile, + EstType.Hair => _estHairFile, + EstType.Body => _estBodyFile, + EstType.Head => _estHeadFile, _ => null, }; } - internal ushort GetEstEntry(MetaFileManager manager, EstManipulation.EstType type, GenderRace genderRace, PrimaryId primaryId) + internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId) { var file = GetEstFile(type); return file != null @@ -96,10 +96,10 @@ public struct EstCache : IDisposable _estManipulations.AddOrReplace(m); var file = m.Slot switch { - EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair), - EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face), - EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body), - EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head), + EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair), + EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face), + EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body), + EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head), _ => throw new ArgumentOutOfRangeException(), }; return m.Apply(file); @@ -114,10 +114,10 @@ public struct EstCache : IDisposable var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def); var file = m.Slot switch { - EstManipulation.EstType.Hair => _estHairFile!, - EstManipulation.EstType.Face => _estFaceFile!, - EstManipulation.EstType.Body => _estBodyFile!, - EstManipulation.EstType.Head => _estHeadFile!, + EstType.Hair => _estHairFile!, + EstType.Face => _estFaceFile!, + EstType.Body => _estBodyFile!, + EstType.Head => _estHeadFile!, _ => throw new ArgumentOutOfRangeException(), }; return manip.Apply(file); diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index f42b72fc..fbca9c0e 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -188,7 +188,7 @@ public class MetaCache : IDisposable, IEnumerable _cmpCache.TemporarilySetFiles(_manager); - public MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(EstType type) => _estCache.TemporarilySetFiles(_manager, type); public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) @@ -208,7 +208,7 @@ public class MetaCache : IDisposable, IEnumerable _estCache.GetEstEntry(_manager, type, genderRace, primaryId); /// Use this when CharacterUtility becomes ready. diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3f3733e0..484d4dd2 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -112,7 +112,7 @@ public partial class ModCollection => _cache?.Meta.TemporarilySetCmpFile() ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); - public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstManipulation.EstType type) + public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 485a76a7..fdd28ef1 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -63,16 +63,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return info.ObjectType switch { ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)], ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Head, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)], ObjectType.Equipment => [baseSkeleton], ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)], ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear - => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], @@ -81,7 +81,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string[] ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations) { // Try to find an EST entry from the manipulations provided. var (gender, race) = info.GenderRace.Split(); @@ -96,13 +96,13 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Entry ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? 0; + ?? EstEntry.Zero; // If there's no entries, we can assume that there's no additional skeleton. - if (targetId == 0) + if (targetId == EstEntry.Zero) return []; - return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; + return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 554cf848..f6157747 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -75,14 +75,14 @@ public partial class TexToolsMeta { var gr = (GenderRace)reader.ReadUInt16(); var id = reader.ReadUInt16(); - var value = reader.ReadUInt16(); + var value = new EstEntry(reader.ReadUInt16()); var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch { - (BodySlot.Face, _) => EstManipulation.EstType.Face, - (BodySlot.Hair, _) => EstManipulation.EstType.Hair, - (_, EquipSlot.Head) => EstManipulation.EstType.Head, - (_, EquipSlot.Body) => EstManipulation.EstType.Body, - _ => (EstManipulation.EstType)0, + (BodySlot.Face, _) => EstType.Face, + (BodySlot.Hair, _) => EstType.Hair, + (_, EquipSlot.Head) => EstType.Head, + (_, EquipSlot.Body) => EstType.Body, + _ => (EstType)0, }; if (!gr.IsValid() || type == 0) continue; diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 09bd2c12..4fb56df6 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -66,7 +66,7 @@ public partial class TexToolsMeta foreach (var attribute in attributes) { var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute); - b.Write(value); + b.Write(value.Value); } } @@ -176,7 +176,7 @@ public partial class TexToolsMeta { b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race)); b.Write(manip.Est.SetId.Id); - b.Write(manip.Est.Entry); + b.Write(manip.Est.Entry.Value); } break; @@ -239,10 +239,10 @@ public partial class TexToolsMeta var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode(); return manip.Slot switch { - EstManipulation.EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", - EstManipulation.EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", - EstManipulation.EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", - EstManipulation.EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", + EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", + EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", + EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", + EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", _ => throw new ArgumentOutOfRangeException(), }; } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 51faa175..71b9165f 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -46,8 +46,8 @@ public partial class TexToolsMeta void Add(RspAttribute attribute, float value) { var def = CmpFile.GetDefault(manager, subRace, attribute); - if (keepDefault || value != def) - ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, value)); + if (keepDefault || value != def.Value) + ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value))); } if (gender == 1) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 8118343d..9a68160b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -212,10 +212,10 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable if (_parent.InInternalResolve) return DisposableContainer.Empty; - return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Face), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Body), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Hair), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstManipulation.EstType.Head)); + return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Face), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Body), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Hair), + data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Head)); } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 236c7051..2b87e688 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -250,30 +250,30 @@ internal partial record ResolveContext _ => 0, }; } - return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Face, faceId); + return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId); case 2: - return ResolveHumanExtraSkeletonData(characterRaceCode, EstManipulation.EstType.Hair, human->HairId); + return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId); case 3: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstManipulation.EstType.Head); + return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head); case 4: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstManipulation.EstType.Body); + return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body); default: return (0, string.Empty, 0); } } - private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstManipulation.EstType type) + private unsafe (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanEquipmentSkeletonData(EquipSlot slot, EstType type) { var human = (Human*)CharacterBase; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); } - private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstManipulation.EstType type, PrimaryId primary) + private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary) { var metaCache = Global.Collection.MetaCache; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; - return (raceCode, EstManipulation.ToName(type), skeletonSet); + return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId); } private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index b265a5e8..96cda496 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -2,6 +2,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Interop.Services; +using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -17,10 +18,10 @@ public sealed unsafe class CmpFile : MetaBaseFile private const int RacialScalingStart = 0x2A800; - public float this[SubRace subRace, RspAttribute attribute] + public RspEntry this[SubRace subRace, RspAttribute attribute] { - get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); - set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; + get => *(RspEntry*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + set => *(RspEntry*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4) = value; } public override void Reset() @@ -39,10 +40,10 @@ public sealed unsafe class CmpFile : MetaBaseFile Reset(); } - public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) + public static RspEntry GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; - return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + return *(RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); } private static int ToRspIndex(SubRace subRace) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index af441b22..ee38ea1e 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -34,26 +34,26 @@ public sealed unsafe class EstFile : MetaBaseFile Removed, } - public ushort this[GenderRace genderRace, ushort setId] + public EstEntry this[GenderRace genderRace, PrimaryId setId] { get { var (idx, exists) = FindEntry(genderRace, setId); if (!exists) - return 0; + return EstEntry.Zero; - return *(ushort*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx); + return *(EstEntry*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx); } set => SetEntry(genderRace, setId, value); } - private void InsertEntry(int idx, GenderRace genderRace, ushort setId, ushort skeletonId) + private void InsertEntry(int idx, GenderRace genderRace, PrimaryId setId, EstEntry skeletonId) { if (Length < Size + EntryDescSize + EntrySize) ResizeResources(Length + IncreaseSize); var control = (Info*)(Data + 4); - var entries = (ushort*)(control + Count); + var entries = (EstEntry*)(control + Count); for (var i = Count - 1; i >= idx; --i) entries[i + 3] = entries[i]; @@ -94,10 +94,10 @@ public sealed unsafe class EstFile : MetaBaseFile [StructLayout(LayoutKind.Sequential, Size = 4)] private struct Info : IComparable { - public readonly ushort SetId; + public readonly PrimaryId SetId; public readonly GenderRace GenderRace; - public Info(GenderRace gr, ushort setId) + public Info(GenderRace gr, PrimaryId setId) { GenderRace = gr; SetId = setId; @@ -106,42 +106,42 @@ public sealed unsafe class EstFile : MetaBaseFile public int CompareTo(Info other) { var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); - return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo(other.SetId); + return genderRaceComparison != 0 ? genderRaceComparison : SetId.Id.CompareTo(other.SetId.Id); } } - private static (int, bool) FindEntry(ReadOnlySpan data, GenderRace genderRace, ushort setId) + private static (int, bool) FindEntry(ReadOnlySpan data, GenderRace genderRace, PrimaryId setId) { var idx = data.BinarySearch(new Info(genderRace, setId)); return idx < 0 ? (~idx, false) : (idx, true); } - private (int, bool) FindEntry(GenderRace genderRace, ushort setId) + private (int, bool) FindEntry(GenderRace genderRace, PrimaryId setId) { var span = new ReadOnlySpan(Data + 4, Count); return FindEntry(span, genderRace, setId); } - public EstEntryChange SetEntry(GenderRace genderRace, ushort setId, ushort skeletonId) + public EstEntryChange SetEntry(GenderRace genderRace, PrimaryId setId, EstEntry skeletonId) { var (idx, exists) = FindEntry(genderRace, setId); if (exists) { - var value = *(ushort*)(Data + 4 * (Count + 1) + 2 * idx); + var value = *(EstEntry*)(Data + 4 * (Count + 1) + 2 * idx); if (value == skeletonId) return EstEntryChange.Unchanged; - if (skeletonId == 0) + if (skeletonId == EstEntry.Zero) { RemoveEntry(idx); return EstEntryChange.Removed; } - *(ushort*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId; + *(EstEntry*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId; return EstEntryChange.Changed; } - if (skeletonId == 0) + if (skeletonId == EstEntry.Zero) return EstEntryChange.Unchanged; InsertEntry(idx, genderRace, setId, skeletonId); @@ -156,7 +156,7 @@ public sealed unsafe class EstFile : MetaBaseFile MemoryUtility.MemSet(Data + length, 0, Length - length); } - public EstFile(MetaFileManager manager, EstManipulation.EstType estType) + public EstFile(MetaFileManager manager, EstType estType) : base(manager, (MetaIndex)estType) { var length = DefaultData.Length; @@ -164,24 +164,24 @@ public sealed unsafe class EstFile : MetaBaseFile Reset(); } - public ushort GetDefault(GenderRace genderRace, ushort setId) + public EstEntry GetDefault(GenderRace genderRace, PrimaryId setId) => GetDefault(Manager, Index, genderRace, setId); - public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId) + public static EstEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, PrimaryId primaryId) { var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address; var count = *(int*)data; var span = new ReadOnlySpan(data + 4, count); var (idx, found) = FindEntry(span, genderRace, primaryId.Id); if (!found) - return 0; + return EstEntry.Zero; - return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize); + return *(EstEntry*)(data + 4 + count * EntryDescSize + idx * EntrySize); } - public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId) + public static EstEntry GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, PrimaryId primaryId) => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, primaryId); - public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, PrimaryId primaryId) + public static EstEntry GetDefault(MetaFileManager manager, EstType estType, GenderRace genderRace, PrimaryId primaryId) => GetDefault(manager, (MetaIndex)estType, genderRace, primaryId); } diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs new file mode 100644 index 00000000..6d6942e6 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -0,0 +1,87 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, GenderRace GenderRace) + : IMetaIdentifier, IComparable +{ + public ModelRace Race + => GenderRace.Split().Item2; + + public Gender Gender + => GenderRace.Split().Item1; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); + + public MetaIndex FileIndex() + => CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory()); + + public override string ToString() + => $"Eqdp - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}"; + + public bool Validate() + { + var mask = Eqdp.Mask(Slot); + if (mask == 0) + return false; + + if (FileIndex() == (MetaIndex)(-1)) + return false; + + // No check for set id. + return true; + } + + public int CompareTo(EqdpIdentifier other) + { + var gr = GenderRace.CompareTo(other.GenderRace); + if (gr != 0) + return gr; + + var set = SetId.Id.CompareTo(other.SetId.Id); + if (set != 0) + return set; + + return Slot.CompareTo(other.Slot); + } + + public static EqdpIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var ret = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} + +public readonly record struct InternalEqdpEntry(bool Model, bool Material) +{ + private InternalEqdpEntry((bool, bool) val) + : this(val.Item1, val.Item2) + { } + + public InternalEqdpEntry(EqdpEntry entry, EquipSlot slot) + : this(entry.ToBits(slot)) + { } + + + public EqdpEntry ToEntry(EquipSlot slot) + => Eqdp.FromSlotAndBits(slot, Model, Material); +} diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 0426dfce..2c01ce3f 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -10,27 +10,30 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct EqdpManipulation : IMetaManipulation { - public EqdpEntry Entry { get; private init; } + [JsonIgnore] + public EqdpIdentifier Identifier { get; private init; } + public EqdpEntry Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender { get; private init; } + public Gender Gender + => Identifier.Gender; [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race { get; private init; } + public ModelRace Race + => Identifier.Race; - public PrimaryId SetId { get; private init; } + public PrimaryId SetId + => Identifier.SetId; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot { get; private init; } + public EquipSlot Slot + => Identifier.Slot; [JsonConstructor] public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) { - Gender = gender; - Race = race; - SetId = setId; - Slot = slot; - Entry = Eqdp.Mask(Slot) & entry; + Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); + Entry = Eqdp.Mask(Slot) & entry; } public EqdpManipulation Copy(EqdpManipulation entry) diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs new file mode 100644 index 00000000..572dc203 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); + + public MetaIndex FileIndex() + => MetaIndex.Eqp; + + public override string ToString() + => $"Eqp - {SetId} - {Slot}"; + + public bool Validate() + { + var mask = Eqp.Mask(Slot); + if (mask == 0) + return false; + + // No check for set id. + return true; + } + + public int CompareTo(EqpIdentifier other) + { + var set = SetId.Id.CompareTo(other.SetId.Id); + if (set != 0) + return set; + + return Slot.CompareTo(other.Slot); + } + + public static EqpIdentifier? FromJson(JObject jObj) + { + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var ret = new EqpIdentifier(setId, slot); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} + +public readonly record struct EqpEntryInternal(uint Value) +{ + public EqpEntryInternal(EqpEntry entry, EquipSlot slot) + : this(GetValue(entry, slot)) + { } + + public EqpEntry ToEntry(EquipSlot slot) + { + var (offset, mask) = Eqp.OffsetAndMask(slot); + return (EqpEntry)((ulong)Value << offset) & mask; + } + + private static uint GetValue(EqpEntry entry, EquipSlot slot) + { + var (offset, mask) = Eqp.OffsetAndMask(slot); + return (uint)((ulong)(entry & mask) >> offset); + } +} diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index d59938b6..3bced096 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -5,7 +5,6 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Util; -using SharpCompress.Common; namespace Penumbra.Meta.Manipulations; @@ -15,17 +14,20 @@ public readonly struct EqpManipulation : IMetaManipulation [JsonConverter(typeof(ForceNumericFlagEnumConverter))] public EqpEntry Entry { get; private init; } - public PrimaryId SetId { get; private init; } + public EqpIdentifier Identifier { get; private init; } + + public PrimaryId SetId + => Identifier.SetId; [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot { get; private init; } + public EquipSlot Slot + => Identifier.Slot; [JsonConstructor] public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) { - Slot = slot; - SetId = setId; - Entry = Eqp.Mask(slot) & entry; + Identifier = new EqpIdentifier(setId, slot); + Entry = Eqp.Mask(slot) & entry; } public EqpManipulation Copy(EqpEntry entry) diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs new file mode 100644 index 00000000..9f878f97 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -0,0 +1,113 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public enum EstType : byte +{ + Hair = MetaIndex.HairEst, + Face = MetaIndex.FaceEst, + Body = MetaIndex.BodyEst, + Head = MetaIndex.HeadEst, +} + +public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, GenderRace GenderRace) + : IMetaIdentifier, IComparable +{ + public ModelRace Race + => GenderRace.Split().Item2; + + public Gender Gender + => GenderRace.Split().Item1; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + switch (Slot) + { + case EstType.Hair: + changedItems.TryAdd( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair (Hair) {SetId}", null); + break; + case EstType.Face: + changedItems.TryAdd( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face (Face) {SetId}", null); + break; + case EstType.Body: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Body)); + break; + case EstType.Head: + identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Head)); + break; + } + } + + public MetaIndex FileIndex() + => (MetaIndex)Slot; + + public override string ToString() + => $"Est - {SetId} - {Slot} - {GenderRace.ToName()}"; + + public bool Validate() + { + if (!Enum.IsDefined(Slot)) + return false; + if (GenderRace is GenderRace.Unknown || !Enum.IsDefined(GenderRace)) + return false; + + // No known check for set id. + return true; + } + + public int CompareTo(EstIdentifier other) + { + var gr = GenderRace.CompareTo(other.GenderRace); + if (gr != 0) + return gr; + + var id = SetId.Id.CompareTo(other.SetId.Id); + return id != 0 ? id : Slot.CompareTo(other.Slot); + } + + public static EstIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var slot = jObj["Slot"]?.ToObject() ?? 0; + var ret = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["SetId"] = SetId.Id.ToString(); + jObj["Slot"] = Slot.ToString(); + return jObj; + } +} + +[JsonConverter(typeof(Converter))] +public readonly record struct EstEntry(ushort Value) +{ + public static readonly EstEntry Zero = new(0); + + public PrimaryId AsId + => new(Value); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, EstEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override EstEntry ReadJson(JsonReader reader, Type objectType, EstEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index d3c92ad3..c3f9792f 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -10,14 +10,6 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct EstManipulation : IMetaManipulation { - public enum EstType : byte - { - Hair = MetaIndex.HairEst, - Face = MetaIndex.FaceEst, - Body = MetaIndex.BodyEst, - Head = MetaIndex.HeadEst, - } - public static string ToName(EstType type) => type switch { @@ -28,31 +20,33 @@ public readonly struct EstManipulation : IMetaManipulation _ => "unk", }; - public ushort Entry { get; private init; } // SkeletonIdx. + public EstIdentifier Identifier { get; private init; } + public EstEntry Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender { get; private init; } + public Gender Gender + => Identifier.Gender; [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race { get; private init; } + public ModelRace Race + => Identifier.Race; - public PrimaryId SetId { get; private init; } + public PrimaryId SetId + => Identifier.SetId; [JsonConverter(typeof(StringEnumConverter))] - public EstType Slot { get; private init; } + public EstType Slot + => Identifier.Slot; [JsonConstructor] - public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, ushort entry) + public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) { - Entry = entry; - Gender = gender; - Race = race; - SetId = setId; - Slot = slot; + Entry = entry; + Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); } - public EstManipulation Copy(ushort entry) + public EstManipulation Copy(EstEntry entry) => new(Gender, Race, Slot, SetId, entry); @@ -111,3 +105,5 @@ public readonly struct EstManipulation : IMetaManipulation return true; } } + + diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs new file mode 100644 index 00000000..1b7c70ba --- /dev/null +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + + public MetaIndex FileIndex() + => MetaIndex.Gmp; + + public override string ToString() + => $"Gmp - {SetId}"; + + public bool Validate() + // No known conditions. + => true; + + public int CompareTo(GmpIdentifier other) + => SetId.Id.CompareTo(other.SetId.Id); + + public static GmpIdentifier? FromJson(JObject jObj) + { + var setId = new PrimaryId(jObj["SetId"]?.ToObject() ?? 0); + var ret = new GmpIdentifier(setId); + return ret.Validate() ? ret : null; + } + + public JObject AddToJson(JObject jObj) + { + jObj["SetId"] = SetId.Id.ToString(); + return jObj; + } +} diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index ee58295d..0b2a9f4b 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -8,14 +8,18 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct GmpManipulation : IMetaManipulation { - public GmpEntry Entry { get; private init; } - public PrimaryId SetId { get; private init; } + public GmpIdentifier Identifier { get; private init; } + + public GmpEntry Entry { get; private init; } + + public PrimaryId SetId + => Identifier.SetId; [JsonConstructor] public GmpManipulation(GmpEntry entry, PrimaryId setId) { - Entry = entry; - SetId = setId; + Entry = entry; + Identifier = new GmpIdentifier(setId); } public GmpManipulation Copy(GmpEntry entry) diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs new file mode 100644 index 00000000..29cdfd71 --- /dev/null +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier +{ + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); + + public MetaIndex FileIndex() + => throw new NotImplementedException(); + + public bool Validate() + => throw new NotImplementedException(); + + public JObject AddToJson(JObject jObj) + => throw new NotImplementedException(); +} + +[JsonConverter(typeof(Converter))] +public readonly record struct RspEntry(float Value) : IComparisonOperators +{ + public const float MinValue = 0.01f; + public const float MaxValue = 512f; + public static readonly RspEntry One = new(1f); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override RspEntry ReadJson(JsonReader reader, Type objectType, RspEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } + + public static bool operator >(RspEntry left, RspEntry right) + => left.Value > right.Value; + + public static bool operator >=(RspEntry left, RspEntry right) + => left.Value >= right.Value; + + public static bool operator <(RspEntry left, RspEntry right) + => left.Value < right.Value; + + public static bool operator <=(RspEntry left, RspEntry right) + => left.Value <= right.Value; +} diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 7e5e3fcb..04691c9f 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -9,25 +9,25 @@ namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] public readonly struct RspManipulation : IMetaManipulation { - public const float MinValue = 0.01f; - public const float MaxValue = 512f; - public float Entry { get; private init; } + public RspIdentifier Identifier { get; private init; } + public RspEntry Entry { get; private init; } [JsonConverter(typeof(StringEnumConverter))] - public SubRace SubRace { get; private init; } + public SubRace SubRace + => Identifier.SubRace; [JsonConverter(typeof(StringEnumConverter))] - public RspAttribute Attribute { get; private init; } + public RspAttribute Attribute + => Identifier.Attribute; [JsonConstructor] - public RspManipulation(SubRace subRace, RspAttribute attribute, float entry) + public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) { - Entry = entry; - SubRace = subRace; - Attribute = attribute; + Entry = entry; + Identifier = new RspIdentifier(subRace, attribute); } - public RspManipulation Copy(float entry) + public RspManipulation Copy(RspEntry entry) => new(SubRace, Attribute, entry); public override string ToString() @@ -68,7 +68,7 @@ public readonly struct RspManipulation : IMetaManipulation return false; if (!Enum.IsDefined(Attribute)) return false; - if (Entry is < MinValue or > MaxValue) + if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue) return false; return true; diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index ea4ef7b1..3efee857 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -126,9 +126,9 @@ public static class EquipmentSwap var isAccessory = slot.IsAccessory(); var estType = slot switch { - EquipSlot.Head => EstManipulation.EstType.Head, - EquipSlot.Body => EstManipulation.EstType.Body, - _ => (EstManipulation.EstType)0, + EquipSlot.Head => EstType.Head, + EquipSlot.Body => EstType.Body, + _ => (EstType)0, }; var skipFemale = false; diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index b269d89c..7fac52c1 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -133,23 +133,23 @@ public static class ItemSwap } - public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstManipulation.EstType type, - GenderRace race, ushort estEntry) + public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, + GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry); + var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } - public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstManipulation.EstType type, - GenderRace race, ushort estEntry) + public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, + GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry); + var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, - Func manips, EstManipulation.EstType type, + Func manips, EstType type, GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) { if (type == 0) @@ -160,7 +160,7 @@ public static class ItemSwap var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); var est = new MetaSwap(manips, fromDefault, toDefault); - if (ownMdl && est.SwapApplied.Est.Entry >= 2) + if (ownMdl && est.SwapApplied.Est.Entry.Value >= 2) { var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); est.ChildSwaps.Add(phyb); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 449405a0..48d687d0 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -152,9 +152,9 @@ public class ItemSwapContainer var mdl = CustomizationSwap.CreateMdl(manager, pathResolver, slot, race, from, to); var type = slot switch { - BodySlot.Hair => EstManipulation.EstType.Hair, - BodySlot.Face => EstManipulation.EstType.Face, - _ => (EstManipulation.EstType)0, + BodySlot.Hair => EstType.Hair, + BodySlot.Face => EstType.Face, + _ => (EstType)0, }; var metaResolver = MetaResolver(collection); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 0783dd98..b0a74637 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -453,7 +453,7 @@ public partial class ModEditWindow private static class EstRow { - private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); + private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); private static float IdWidth => 100 * UiHelpers.Scale; @@ -510,7 +510,7 @@ public partial class ModEditWindow // Values using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); - IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f); + IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f); } public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) @@ -538,9 +538,9 @@ public partial class ModEditWindow // Values var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, + if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, out var entry, 0, ushort.MaxValue, 0.05f)) - editor.MetaEditor.Change(meta.Copy((ushort)entry)); + editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); } } @@ -646,7 +646,7 @@ public partial class ModEditWindow private static class RspRow { - private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); + private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); private static float FloatWidth => 150 * UiHelpers.Scale; @@ -680,7 +680,8 @@ public partial class ModEditWindow using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(FloatWidth); - ImGui.DragFloat("##rspValue", ref defaultEntry, 0f); + var value = defaultEntry.Value; + ImGui.DragFloat("##rspValue", ref value, 0f); } public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) @@ -699,15 +700,15 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute); - var value = meta.Entry; + var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; + var value = meta.Entry.Value; ImGui.SetNextItemWidth(FloatWidth); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspManipulation.MinValue, RspManipulation.MaxValue) - && value is >= RspManipulation.MinValue and <= RspManipulation.MaxValue) - editor.MetaEditor.Change(meta.Copy(value)); + if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue) + && value is >= RspEntry.MinValue and <= RspEntry.MaxValue) + editor.MetaEditor.Change(meta.Copy(new RspEntry(value))); ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); } diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 253bf0e0..234f7a3e 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -33,7 +33,7 @@ public static class Combos => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute, RspAttributeExtensions.ToFullString, 0, 1); - public static bool EstSlot(string label, EstManipulation.EstType current, out EstManipulation.EstType attribute, float unscaledWidth = 200) + public static bool EstSlot(string label, EstType current, out EstType attribute, float unscaledWidth = 200) => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * UiHelpers.Scale, current, out attribute); public static bool ImcType(string label, ObjectType current, out ObjectType type, float unscaledWidth = 110) diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 7368c7c8..0647ea8e 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -51,18 +51,18 @@ public static class IdentifierExtensions case MetaManipulation.Type.Est: switch (manip.Est.Slot) { - case EstManipulation.EstType.Hair: + case EstType.Hair: changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); break; - case EstManipulation.EstType.Face: + case EstType.Face: changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); break; - case EstManipulation.EstType.Body: + case EstType.Body: identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), EquipSlot.Body)); break; - case EstManipulation.EstType.Head: + case EstType.Head: identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), EquipSlot.Head)); From 50a7e7efb7f0ea41583ab04b1dd6dd16ca2f992e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 7 Jun 2024 16:34:09 +0200 Subject: [PATCH 142/865] Add more filter options. --- OtterGui | 2 +- .../Meta/Manipulations/ImcManipulation.cs | 1 - Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 106 ++++---------- .../UI/ModsTab/ModSearchStringSplitter.cs | 138 ++++++++++++++++++ 4 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 Penumbra/UI/ModsTab/ModSearchStringSplitter.cs diff --git a/OtterGui b/OtterGui index 5de708b2..ac176daf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5de708b27ed45c9cdead71742c7061ad9ce64323 +Subproject commit ac176daf068f42d0b04a77dbc149f68a425fd460 diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs index eb3720c9..5065a06e 100644 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ b/Penumbra/Meta/Manipulations/ImcManipulation.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 5b6cfa99..58f0b615 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -23,17 +23,20 @@ namespace Penumbra.UI.ModsTab; public sealed class ModFileSystemSelector : FileSystemSelector { - private readonly CommunicatorService _communicator; - private readonly MessageService _messager; - private readonly Configuration _config; - private readonly FileDialogService _fileDialog; - private readonly ModManager _modManager; - private readonly CollectionManager _collectionManager; - private readonly TutorialService _tutorial; - private readonly ModImportManager _modImportManager; - private readonly IDragDropManager _dragDrop; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + private readonly CommunicatorService _communicator; + private readonly MessageService _messager; + private readonly Configuration _config; + private readonly FileDialogService _fileDialog; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly TutorialService _tutorial; + private readonly ModImportManager _modImportManager; + private readonly IDragDropManager _dragDrop; + private readonly ModSearchStringSplitter Filter = new(); + + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, @@ -568,78 +571,49 @@ public sealed class ModFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. protected override bool ChangeFilter(string filterValue) { - (_modFilter, _filterType) = filterValue.Length switch - { - 0 => (LowerString.Empty, -1), - > 1 when filterValue[1] == ':' => - filterValue[0] switch - { - 'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), - 'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), - 'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), - 'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2), - 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), - 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3), - 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), - 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4), - 's' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5), - 'S' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5), - _ => (new LowerString(filterValue), 0), - }, - _ => (new LowerString(filterValue), 0), - }; - + Filter.Parse(filterValue); return true; } - private const int EmptyOffset = 128; - - private (LowerString, int) ParseFilter(string value, int id) - { - value = value[2..]; - var lower = new LowerString(value); - if (id == 5 && !ChangedItemDrawer.TryParsePartial(lower.Lower, out _slotFilter)) - _slotFilter = 0; - - return (lower, lower.Lower is "none" ? id + EmptyOffset : id); - } - - /// /// Check the state filter for a specific pair of has/has-not flags. /// Uses count == 0 to check for has-not and count != 0 for has. /// Returns true if it should be filtered and false if not. /// private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag) - { - return count switch + => count switch { 0 when _stateFilter.HasFlag(hasNoFlag) => false, 0 => true, _ when _stateFilter.HasFlag(hasFlag) => false, _ => true, }; - } /// /// The overwritten filter method also computes the state. @@ -653,7 +627,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0 && !f.FullName().Contains(FilterValue, IgnoreCase); + || !Filter.IsVisible(f); } return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state); @@ -661,23 +635,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Apply the string filters. private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) - { - return _filterType switch - { - -1 => false, - 0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), - 1 => !mod.Name.Contains(_modFilter), - 2 => !mod.Author.Contains(_modFilter), - 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), - 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), - 5 => mod.ChangedItems.All(p => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & _slotFilter) == 0), - 2 + EmptyOffset => !mod.Author.IsEmpty, - 3 + EmptyOffset => mod.LowerChangedItemsString.Length > 0, - 4 + EmptyOffset => mod.AllTagsLower.Length > 0, - 5 + EmptyOffset => mod.ChangedItems.Count == 0, - _ => false, // Should never happen - }; - } + => !Filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs new file mode 100644 index 00000000..1ea70731 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -0,0 +1,138 @@ +using OtterGui.Filesystem; +using OtterGui.Filesystem.Selector; +using Penumbra.Mods; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI.ModsTab; + +public enum ModSearchType : byte +{ + Default = 0, + ChangedItem, + Tag, + Name, + Author, + Category, +} + +public sealed class ModSearchStringSplitter : SearchStringSplitter.Leaf, ModSearchStringSplitter.Entry> +{ + public readonly struct Entry : ISplitterEntry + { + public string Needle { get; init; } + public ModSearchType Type { get; init; } + public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; } + + public bool Contains(Entry other) + { + if (Type != other.Type) + return false; + if (Type is ModSearchType.Category) + return IconFilter == other.IconFilter; + + return Needle.Contains(other.Needle); + } + } + + protected override bool ConvertToken(char token, out ModSearchType val) + { + val = token switch + { + 'c' or 'C' => ModSearchType.ChangedItem, + 't' or 'T' => ModSearchType.Tag, + 'n' or 'N' => ModSearchType.Name, + 'a' or 'A' => ModSearchType.Author, + 's' or 'S' => ModSearchType.Category, + _ => ModSearchType.Default, + }; + return val is not ModSearchType.Default; + } + + protected override bool AllowsNone(ModSearchType val) + => val switch + { + ModSearchType.Author => true, + ModSearchType.ChangedItem => true, + ModSearchType.Tag => true, + ModSearchType.Category => true, + _ => false, + }; + + protected override void PostProcessing() + { + base.PostProcessing(); + HandleList(General); + HandleList(Forced); + HandleList(Negated); + return; + + static void HandleList(List list) + { + for (var i = 0; i < list.Count; ++i) + { + var entry = list[i]; + if (entry.Type is not ModSearchType.Category) + continue; + + if (ChangedItemDrawer.TryParsePartial(entry.Needle, out var icon)) + list[i] = entry with + { + IconFilter = icon, + Needle = string.Empty, + }; + else + list.RemoveAt(i--); + } + } + } + + public bool IsVisible(ModFileSystem.Folder folder) + { + switch (State) + { + case FilterState.NoFilters: return true; + case FilterState.NoMatches: return false; + } + + var fullName = folder.FullName(); + return Forced.All(i => MatchesName(i, folder.Name, fullName)) + && !Negated.Any(i => MatchesName(i, folder.Name, fullName)) + && (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName))); + } + + protected override bool Matches(Entry entry, ModFileSystem.Leaf leaf) + => entry.Type switch + { + ModSearchType.Default => leaf.FullName().AsSpan().Contains(entry.Needle, StringComparison.OrdinalIgnoreCase) + || leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.ChangedItem => leaf.Value.LowerChangedItemsString.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Tag => leaf.Value.AllTagsLower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Name => leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), + ModSearchType.Category => leaf.Value.ChangedItems.Any(p + => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0), + _ => true, + }; + + protected override bool MatchesNone(ModSearchType type, bool negated, ModFileSystem.Leaf haystack) + => type switch + { + ModSearchType.Author when negated => !haystack.Value.Author.IsEmpty, + ModSearchType.Author => haystack.Value.Author.IsEmpty, + ModSearchType.ChangedItem when negated => haystack.Value.LowerChangedItemsString.Length > 0, + ModSearchType.ChangedItem => haystack.Value.LowerChangedItemsString.Length == 0, + ModSearchType.Tag when negated => haystack.Value.AllTagsLower.Length > 0, + ModSearchType.Tag => haystack.Value.AllTagsLower.Length == 0, + ModSearchType.Category when negated => haystack.Value.ChangedItems.Count > 0, + ModSearchType.Category => haystack.Value.ChangedItems.Count == 0, + _ => true, + }; + + private static bool MatchesName(Entry entry, ReadOnlySpan name, ReadOnlySpan fullName) + => entry.Type switch + { + ModSearchType.Default => fullName.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), + ModSearchType.Name => name.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), + _ => false, + }; +} From 102e7335a7303526a6ae71c5f89194538c8c9f56 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 7 Jun 2024 14:40:25 +0000 Subject: [PATCH 143/865] [CI] Updating repo.json for testing_1.1.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 5e9e5b37..5d6a9ed8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.0.2", - "TestingAssemblyVersion": "1.1.0.2", + "TestingAssemblyVersion": "1.1.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From de0309bfa7abcc925209086cbf88360bfaee1ee0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 12:45:12 +0200 Subject: [PATCH 144/865] Fix issue with ImcGroup settings and IPC. --- Penumbra/Api/Api/ModSettingsApi.cs | 27 +++++++++++++------------- Penumbra/Mods/Manager/ModDataEditor.cs | 6 ++---- Penumbra/Mods/Settings/ModSettings.cs | 4 ++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 56b80e63..e046ce30 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -77,10 +77,10 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_collectionManager.Storage.ById(collectionId, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - var settings = collection.Id == Guid.Empty - ? null - : ignoreInheritance - ? collection.Settings[mod.Index] + var settings = collection.Id == Guid.Empty + ? null + : ignoreInheritance + ? collection.Settings[mod.Index] : collection[mod.Index].Settings; if (settings == null) return (PenumbraApiEc.Success, null); @@ -160,11 +160,11 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - var setting = mod.Groups[groupIdx] switch + var setting = mod.Groups[groupIdx].Behaviour switch { - MultiModGroup => Setting.Multi(optionIdx), - SingleModGroup => Setting.Single(optionIdx), - _ => Setting.Zero, + GroupDrawBehaviour.MultiSelection => Setting.Multi(optionIdx), + GroupDrawBehaviour.SingleSelection => Setting.Single(optionIdx), + _ => Setting.Zero, }; var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success @@ -191,20 +191,20 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var setting = Setting.Zero; switch (mod.Groups[groupIdx]) { - case SingleModGroup single: + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: { - var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]); + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); setting = Setting.Single(optionIdx); break; } - case MultiModGroup multi: + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: { foreach (var name in optionNames) { - var optionIdx = multi.OptionData.IndexOf(o => o.Mod.Name == name); + var optionIdx = multi.Options.IndexOf(o => o.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -256,7 +256,8 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int moveIndex) { switch (type) { diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index e0af6f36..c7c7c2cc 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -50,7 +50,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var save = true; if (File.Exists(dataFile)) { - save = false; try { var text = File.ReadAllText(dataFile); @@ -60,6 +59,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; note = json[nameof(Mod.Note)]?.Value() ?? note; localTags = json[nameof(Mod.LocalTags)]?.Values().OfType() ?? localTags; + save = false; } catch (Exception e) { @@ -239,7 +239,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Favorite = state; saveService.QueueSave(new ModLocalData(mod)); - ; communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -250,7 +249,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Note = newNote; saveService.QueueSave(new ModLocalData(mod)); - ; communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -260,7 +258,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic if (tagIdx < 0 || tagIdx > which.Count) return; - ModDataChangeType flags = 0; + ModDataChangeType flags; if (tagIdx == which.Count) { flags = ModLocalData.UpdateTags(mod, local ? null : which.Append(newTag), local ? which.Append(newTag) : null); diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 7fe48365..25e4805d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -185,10 +185,10 @@ public class ModSettings switch (mod.Groups[idx]) { - case SingleModGroup single when setting.Value < (ulong)single.Options.Count: + case { Behaviour: GroupDrawBehaviour.SingleSelection } single when setting.Value < (ulong)single.Options.Count: dict.Add(single.Name, [single.Options[setting.AsIndex].Name]); break; - case MultiModGroup multi: + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: var list = multi.Options.WithIndex().Where(p => setting.HasFlag(p.Index)).Select(p => p.Value.Name).ToList(); dict.Add(multi.Name, list); break; From e884b269a9ca3f2ea4c6ef63cf7da9e53d1353cb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 14:55:42 +0200 Subject: [PATCH 145/865] Add a version field to mod group files. --- Penumbra/Mods/Groups/ModSaveGroup.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 05437e3d..c82c67c7 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -6,6 +6,8 @@ namespace Penumbra.Mods.Groups; public readonly struct ModSaveGroup : ISavable { + public const int CurrentVersion = 0; + private readonly DirectoryInfo _basePath; private readonly IModGroup? _group; private readonly int _groupIdx; @@ -71,6 +73,8 @@ public readonly struct ModSaveGroup : ISavable j.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; j.WriteStartObject(); + j.WritePropertyName("Version"); + j.WriteValue(CurrentVersion); if (_groupIdx >= 0) _group!.WriteJson(j, serializer, _basePath); else From 159942f29c229f3dafef7275c553cc9f7e2183b6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 20:33:07 +0200 Subject: [PATCH 146/865] Add by-name identification in the lobby. --- .../PathResolving/CollectionResolver.cs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index aea58304..ed111794 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -7,6 +8,8 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.String; using Penumbra.Util; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -94,14 +97,8 @@ public sealed unsafe class CollectionResolver( public bool IsModelHuman(uint modelCharaId) => humanModels.IsHuman(modelCharaId); - /// Return whether the given character has a human model. - public bool IsModelHuman(Character* character) - => character != null && IsModelHuman((uint)character->CharacterData.ModelCharaId); - /// - /// Used if on the Login screen. Names are populated after actors are drawn, - /// so it is not possible to fetch names from the ui list. - /// Actors are also not named. So use Yourself > Players > Racial > Default. + /// Used if on the Login screen. /// private bool LoginScreen(GameObject* gameObject, out ResolveData ret) { @@ -114,6 +111,27 @@ public sealed unsafe class CollectionResolver( } var notYetReady = false; + var lobby = AgentLobby.Instance(); + if (lobby != null) + { + var span = lobby->LobbyData.CharaSelectEntries.Span; + // The lobby uses the first 8 cutscene actors. + var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; + if (idx >= 0 && idx < span.Length && span[idx].Value != null) + { + var item = span[idx].Value; + var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); + Penumbra.Log.Verbose( + $"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}."); + if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll) + { + // Do not add this to caches because game objects are reused for different draw objects. + ret = coll.ToResolveData(gameObject); + return true; + } + } + } + var collection = collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? collectionManager.Active.Default; @@ -189,7 +207,7 @@ public sealed unsafe class CollectionResolver( return null; // Only handle human models. - + if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) return null; From f51fc2cafd985cf3b3cca605f74b85639cf8036f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:05:19 +0200 Subject: [PATCH 147/865] Allow root directory overwriting with case sensitivity. --- Penumbra/Mods/Manager/ModManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index adaca85e..010cad19 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -266,7 +266,7 @@ public sealed class ModManager : ModStorage, IDisposable /// private void SetBaseDirectory(string newPath, bool firstTime) { - if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.OrdinalIgnoreCase)) + if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.Ordinal)) return; if (newPath.Length == 0) From f6b35497c5610acc00663ea31e9c69190ff3e357 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:05:37 +0200 Subject: [PATCH 148/865] Change path comparison for AddMod. --- Penumbra/Api/Api/ModsApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 16dd8be9..548831d5 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -75,7 +75,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable if (!dir.Exists) return ApiHelpers.Return(PenumbraApiEc.FileMissing, args); - if (_modManager.BasePath.FullName != dir.Parent?.FullName) + if (dir.Parent == null || Path.GetFullPath(_modManager.BasePath.FullName) != Path.GetFullPath(dir.Parent.FullName)) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); _modManager.AddMod(dir); From f7adc83d631936bf76446af1205a333baa260c44 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:06:18 +0200 Subject: [PATCH 149/865] Fix issue with preview of file in advanced model editing. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index a7d39c6e..bbed64b7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Components; using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; @@ -27,44 +26,56 @@ public partial class ModEditWindow private readonly FileEditor _modelTab; private readonly ModelManager _models; + private class LoadedData + { + public MdlFile LastFile = null!; + public readonly List SubMeshAttributeTags = []; + public long[] LodTriCount = []; + } + private string _modelNewMaterial = string.Empty; - private readonly List _subMeshAttributeTagWidgets = []; + + private readonly LoadedData _main = new(); + private readonly LoadedData _preview = new(); + private string _customPath = string.Empty; private Utf8GamePath _customGamePath = Utf8GamePath.Empty; - private MdlFile _lastFile = null!; - private long[] _lodTriCount = []; - private void UpdateFile(MdlFile file, bool force) + + + private LoadedData UpdateFile(MdlFile file, bool force, bool disabled) { - if (file == _lastFile && !force) - return; + var data = disabled ? _preview : _main; + if (file == data.LastFile && !force) + return data; - _lastFile = file; + data.LastFile = file; var subMeshTotal = file.Meshes.Aggregate(0, (count, mesh) => count + mesh.SubMeshCount); - if (_subMeshAttributeTagWidgets.Count != subMeshTotal) + if (data.SubMeshAttributeTags.Count != subMeshTotal) { - _subMeshAttributeTagWidgets.Clear(); - _subMeshAttributeTagWidgets.AddRange( + data.SubMeshAttributeTags.Clear(); + data.SubMeshAttributeTags.AddRange( Enumerable.Range(0, subMeshTotal).Select(_ => new TagButtons()) ); } - _lodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + data.LodTriCount = Enumerable.Range(0, file.Lods.Length).Select(l => GetTriangleCountForLod(file, l)).ToArray(); + return data; } private bool DrawModelPanel(MdlTab tab, bool disabled) { var ret = tab.Dirty; - UpdateFile(tab.Mdl, ret); + var data = UpdateFile(tab.Mdl, ret, disabled); DrawImportExport(tab, disabled); ret |= DrawModelMaterialDetails(tab, disabled); - if (ImGui.CollapsingHeader($"Meshes ({_lastFile.Meshes.Length})###meshes")) - for (var i = 0; i < _lastFile.LodCount; ++i) + if (ImGui.CollapsingHeader($"Meshes ({data.LastFile.Meshes.Length})###meshes")) + for (var i = 0; i < data.LastFile.LodCount; ++i) ret |= DrawModelLodDetails(tab, i, disabled); - ret |= DrawOtherModelDetails(disabled); + ret |= DrawOtherModelDetails(data); return !disabled && ret; } @@ -98,7 +109,7 @@ public partial class ModEditWindow return true; }); - using (var frame = ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) + using (ImRaii.FramedGroup("Import", size, headerPreIcon: FontAwesomeIcon.FileImport)) { ImGui.Checkbox("Keep current materials", ref tab.ImportKeepMaterials); ImGui.Checkbox("Keep current attributes", ref tab.ImportKeepAttributes); @@ -232,7 +243,7 @@ public partial class ModEditWindow if (!ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256)) return; - if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false)) + if (!Utf8GamePath.FromString(_customPath, out _customGamePath)) _customGamePath = Utf8GamePath.Empty; } @@ -399,9 +410,9 @@ public partial class ModEditWindow return ret; } - private void DrawInvalidMaterialMarker() + private static void DrawInvalidMaterialMarker() { - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); ImGuiUtil.HoverTooltip( @@ -534,7 +545,8 @@ public partial class ModEditWindow ImGui.TextUnformatted($"Attributes #{subMeshOffset + 1}"); ImGui.TableNextColumn(); - var widget = _subMeshAttributeTagWidgets[subMeshIndex]; + var data = disabled ? _preview : _main; + var widget = data.SubMeshAttributeTags[subMeshIndex]; var attributes = tab.GetSubMeshAttributes(subMeshIndex); if (attributes == null) @@ -555,7 +567,7 @@ public partial class ModEditWindow return true; } - private bool DrawOtherModelDetails(bool _) + private bool DrawOtherModelDetails(LoadedData data) { using var header = ImRaii.CollapsingHeader("Further Content"); if (!header) @@ -566,44 +578,44 @@ public partial class ModEditWindow if (table) { ImGuiUtil.DrawTableColumn("Version"); - ImGuiUtil.DrawTableColumn($"0x{_lastFile.Version:X}"); + ImGuiUtil.DrawTableColumn($"0x{data.LastFile.Version:X}"); ImGuiUtil.DrawTableColumn("Radius"); - ImGuiUtil.DrawTableColumn(_lastFile.Radius.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(data.LastFile.Radius.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Model Clip Out Distance"); - ImGuiUtil.DrawTableColumn(_lastFile.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(data.LastFile.ModelClipOutDistance.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("Shadow Clip Out Distance"); - ImGuiUtil.DrawTableColumn(_lastFile.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); + ImGuiUtil.DrawTableColumn(data.LastFile.ShadowClipOutDistance.ToString(CultureInfo.InvariantCulture)); ImGuiUtil.DrawTableColumn("LOD Count"); - ImGuiUtil.DrawTableColumn(_lastFile.LodCount.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.LodCount.ToString()); ImGuiUtil.DrawTableColumn("Enable Index Buffer Streaming"); - ImGuiUtil.DrawTableColumn(_lastFile.EnableIndexBufferStreaming.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.EnableIndexBufferStreaming.ToString()); ImGuiUtil.DrawTableColumn("Enable Edge Geometry"); - ImGuiUtil.DrawTableColumn(_lastFile.EnableEdgeGeometry.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.EnableEdgeGeometry.ToString()); ImGuiUtil.DrawTableColumn("Flags 1"); - ImGuiUtil.DrawTableColumn(_lastFile.Flags1.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Flags1.ToString()); ImGuiUtil.DrawTableColumn("Flags 2"); - ImGuiUtil.DrawTableColumn(_lastFile.Flags2.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Flags2.ToString()); ImGuiUtil.DrawTableColumn("Vertex Declarations"); - ImGuiUtil.DrawTableColumn(_lastFile.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Bone Bounding Boxes"); - ImGuiUtil.DrawTableColumn(_lastFile.BoneBoundingBoxes.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.BoneBoundingBoxes.Length.ToString()); ImGuiUtil.DrawTableColumn("Bone Tables"); - ImGuiUtil.DrawTableColumn(_lastFile.BoneTables.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.BoneTables.Length.ToString()); ImGuiUtil.DrawTableColumn("Element IDs"); - ImGuiUtil.DrawTableColumn(_lastFile.ElementIds.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.ElementIds.Length.ToString()); ImGuiUtil.DrawTableColumn("Extra LoDs"); - ImGuiUtil.DrawTableColumn(_lastFile.ExtraLods.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.ExtraLods.Length.ToString()); ImGuiUtil.DrawTableColumn("Meshes"); - ImGuiUtil.DrawTableColumn(_lastFile.Meshes.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Meshes.Length.ToString()); ImGuiUtil.DrawTableColumn("Shape Meshes"); - ImGuiUtil.DrawTableColumn(_lastFile.ShapeMeshes.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.ShapeMeshes.Length.ToString()); ImGuiUtil.DrawTableColumn("LoDs"); - ImGuiUtil.DrawTableColumn(_lastFile.Lods.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.Lods.Length.ToString()); ImGuiUtil.DrawTableColumn("Vertex Declarations"); - ImGuiUtil.DrawTableColumn(_lastFile.VertexDeclarations.Length.ToString()); + ImGuiUtil.DrawTableColumn(data.LastFile.VertexDeclarations.Length.ToString()); ImGuiUtil.DrawTableColumn("Stack Size"); - ImGuiUtil.DrawTableColumn(_lastFile.StackSize.ToString()); - foreach (var (triCount, lod) in _lodTriCount.WithIndex()) + ImGuiUtil.DrawTableColumn(data.LastFile.StackSize.ToString()); + foreach (var (triCount, lod) in data.LodTriCount.WithIndex()) { ImGuiUtil.DrawTableColumn($"LOD #{lod + 1} Triangle Count"); ImGuiUtil.DrawTableColumn(triCount.ToString()); @@ -614,36 +626,36 @@ public partial class ModEditWindow using (var materials = ImRaii.TreeNode("Materials", ImGuiTreeNodeFlags.DefaultOpen)) { if (materials) - foreach (var material in _lastFile.Materials) + foreach (var material in data.LastFile.Materials) ImRaii.TreeNode(material, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) - foreach (var attribute in _lastFile.Attributes) + foreach (var attribute in data.LastFile.Attributes) ImRaii.TreeNode(attribute, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var bones = ImRaii.TreeNode("Bones", ImGuiTreeNodeFlags.DefaultOpen)) { if (bones) - foreach (var bone in _lastFile.Bones) + foreach (var bone in data.LastFile.Bones) ImRaii.TreeNode(bone, ImGuiTreeNodeFlags.Leaf).Dispose(); } using (var shapes = ImRaii.TreeNode("Shapes", ImGuiTreeNodeFlags.DefaultOpen)) { if (shapes) - foreach (var shape in _lastFile.Shapes) + foreach (var shape in data.LastFile.Shapes) ImRaii.TreeNode(shape.ShapeName, ImGuiTreeNodeFlags.Leaf).Dispose(); } - if (_lastFile.RemainingData.Length > 0) + if (data.LastFile.RemainingData.Length > 0) { - using var t = ImRaii.TreeNode($"Additional Data (Size: {_lastFile.RemainingData.Length})###AdditionalData"); + using var t = ImRaii.TreeNode($"Additional Data (Size: {data.LastFile.RemainingData.Length})###AdditionalData"); if (t) - Widget.DrawHexViewer(_lastFile.RemainingData); + Widget.DrawHexViewer(data.LastFile.RemainingData); } return false; From ecd5752d16d14f64ed15775a43459b2006c78593 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Jun 2024 22:12:55 +0200 Subject: [PATCH 150/865] Make imc attribute letter tooltip appear on disabled. --- Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index d346e05c..5c8edce6 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -148,7 +148,7 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr } } - ImUtf8.HoverTooltip("ABCDEFGHIJ"u8.Slice(i, 1)); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1)); if (i != 9) ImUtf8.SameLineInner(); } From 30a87e3f402495517d067235f8235fbf46c3ba81 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 12:28:33 +0200 Subject: [PATCH 151/865] Update valid world check. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index ac176daf..e95c0f04 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ac176daf068f42d0b04a77dbc149f68a425fd460 +Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8 diff --git a/Penumbra.GameData b/Penumbra.GameData index ad12ddce..6aeae346 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ad12ddcef38a9ed4e4dd7424d748f41c4b97db10 +Subproject commit 6aeae346332a255b7575ccfca554afb0f3cf1494 From 863a7edf0ee57c2e5abadb59cad8cd4d85e5272d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 12:50:13 +0200 Subject: [PATCH 152/865] Add changelog. --- Penumbra/Penumbra.cs | 1 - Penumbra/UI/Changelog.cs | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 42be0aa3..3bbfdf65 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -20,7 +20,6 @@ using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; using Penumbra.GameData.Enums; using Penumbra.UI; -using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra; diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 3f5a446a..184633f2 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -50,10 +50,39 @@ public class PenumbraChangelog AddDummy(Changelog); AddDummy(Changelog); Add1_1_0_0(Changelog); - } - + Add1_1_1_0(Changelog); + } + #region Changelogs + private static void Add1_1_1_0(Changelog log) + => log.NextVersion("Version 1.1.1.0") + .RegisterHighlight("Filtering for mods is now tokenized and can filter for multiple things at once, or exclude specific things.") + .RegisterEntry("Hover over the filter to see the new available options in the tooltip.", 1) + .RegisterEntry("Be aware that the tokenization changed the prior behavior slightly.", 1) + .RegisterEntry("This is open to improvements, if you have any ideas, let me know!", 1) + .RegisterHighlight("Added initial identification of characters in the login-screen by name.") + .RegisterEntry("Those characters can not be redrawn and re-use some things, so this may not always behave as expected, but should work in general. Let me know if you encounter edge cases!", 1) + .RegisterEntry("Added functionality for IMC groups to apply to all variants for a model instead of a specific one.") + .RegisterEntry("Improved the resource tree view with filters and incognito mode. (by Ny)") + .RegisterEntry("Added a tooltip to the global EQP condition.") + .RegisterEntry("Fixed the new worlds not being identified correctly because Square Enix could not be bothered to turn them public.") + .RegisterEntry("Fixed model import getting stuck when doing weight adjustments. (by ackwell)") + .RegisterEntry("Fixed an issue with dye previews in the material editor not applying.") + .RegisterEntry("Fixed an issue with collections not saving on renames.") + .RegisterEntry("Fixed an issue parsing collections with settings set to negative values, which should now be set to 0.") + .RegisterEntry("Fixed an issue with the accessory VFX addition.") + .RegisterEntry("Fixed an issue with GMP animation type entries.") + .RegisterEntry("Fixed another issue with the mod merger.") + .RegisterEntry("Fixed an issue with IMC groups and IPC.") + .RegisterEntry("Fixed some issues with the capitalization of the root directory.") + .RegisterEntry("Fixed IMC attribute tooltips not appearing for disabled checkboxes.") + .RegisterEntry("Added GetChangedItems IPC for single mods. (1.1.0.2)") + .RegisterEntry("Fixed an issue with creating unnamed collections. (1.1.0.2)") + .RegisterEntry("Fixed an issue with the mod merger. (1.1.0.2)") + .RegisterEntry("Fixed the global EQP entry for rings checking for bracelets instead of rings. (1.1.0.2)") + .RegisterEntry("Fixed an issue with newly created collections not being added to the collection list. (1.1.0.1)"); + private static void Add1_1_0_0(Changelog log) => log.NextVersion("Version 1.1.0.0") .RegisterImportant( From c8ea33f8dddff7a8ee7bc92791a54c3d20c45f12 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 11 Jun 2024 10:52:49 +0000 Subject: [PATCH 153/865] [CI] Updating repo.json for 1.1.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5d6a9ed8..05de7ec7 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.1.0.2", - "TestingAssemblyVersion": "1.1.0.3", + "AssemblyVersion": "1.1.1.0", + "TestingAssemblyVersion": "1.1.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.0.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.0.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 447735f6091db767d5d06a39cb8959b3aaebc279 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 16:32:29 +0200 Subject: [PATCH 154/865] Add a configuration to disable showing mods in the lobby and at the aesthetician. --- Penumbra/Configuration.cs | 1 + .../Interop/PathResolving/CollectionResolver.cs | 13 +++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 3 +++ 3 files changed, 17 insertions(+) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index b81e84d8..02286cc7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -41,6 +41,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; + public bool ShowModsInLobby { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; public bool UseCharacterCollectionInInspect { get; set; } = true; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index ed111794..b42571ac 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using Microsoft.VisualBasic; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -110,6 +111,12 @@ public sealed unsafe class CollectionResolver( return false; } + if (!config.ShowModsInLobby) + { + ret = ModCollection.Empty.ToResolveData(gameObject); + return true; + } + var notYetReady = false; var lobby = AgentLobby.Instance(); if (lobby != null) @@ -148,6 +155,12 @@ public sealed unsafe class CollectionResolver( return false; } + if (!config.ShowModsInLobby) + { + ret = ModCollection.Empty.ToResolveData(gameObject); + return true; + } + var player = actors.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 30384538..9989f90a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -449,6 +449,9 @@ public class SettingsTab : ITab Checkbox("Use Interface Collection for other Plugin UIs", "Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.", _dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set); + Checkbox($"Use {TutorialService.AssignedCollections} in Lobby", + "If this is disabled, no mods are applied to characters in the lobby or at the aesthetician.", + _config.ShowModsInLobby, v => _config.ShowModsInLobby = v); Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v); From 2346b7588a49049012f240eec2c1cad912c4b86c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 13:39:25 +0200 Subject: [PATCH 155/865] Fix GMP bug. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 6aeae346..0a2e2650 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6aeae346332a255b7575ccfca554afb0f3cf1494 +Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab From 532e8a0936acd38cdfe1701bc62c3f57d06c58ff Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 14 Jun 2024 12:11:16 +0000 Subject: [PATCH 156/865] [CI] Updating repo.json for 1.1.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 05de7ec7..318aafc2 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.1.1.0", - "TestingAssemblyVersion": "1.1.1.0", + "AssemblyVersion": "1.1.1.1", + "TestingAssemblyVersion": "1.1.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b1a059038221960133882e7b45dc697efd694981 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 23:08:04 +0200 Subject: [PATCH 157/865] Make modmerger file lookup case insensitive. --- Penumbra/Mods/Editor/ModMerger.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 8d47051c..74f182d3 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -196,7 +196,7 @@ public class ModMerger : IDisposable { if (fromFileToFile) { - if (!_fileToFile.TryGetValue(input.FullName, out var s)) + if (!_fileToFile.TryGetValue(input.FullName.ToLowerInvariant(), out var s)) { ret = input; return false; @@ -238,7 +238,7 @@ public class ModMerger : IDisposable Directory.CreateDirectory(finalDir); file.CopyTo(path); Penumbra.Log.Verbose($"[Merger] Copied file {file.FullName} to {path}."); - _fileToFile.Add(file.FullName, path); + _fileToFile.Add(file.FullName.ToLowerInvariant(), path); } } From ec207bdba2c01448250f6cbc3dc8e980a720fac1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 20:48:37 +0200 Subject: [PATCH 158/865] Force saves independent of manipulations for swaps and merges. --- Penumbra/Mods/Editor/ModMerger.cs | 18 +++++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 3 ++- .../Manager/OptionEditor/ModGroupEditor.cs | 4 ++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 74f182d3..f5fc9cd7 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -189,7 +189,8 @@ public class ModMerger : IDisposable _editor.SetFiles(option, redirections, SaveType.None); _editor.SetFileSwaps(option, swaps, SaveType.None); - _editor.SetManipulations(option, manips, SaveType.ImmediateSync); + _editor.SetManipulations(option, manips, SaveType.None); + _editor.ForceSave(option, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -263,9 +264,10 @@ public class ModMerger : IDisposable if (mods.Count == 1) { var files = CopySubModFiles(mods[0], dir); - _editor.SetFiles(result.Default, files); - _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); - _editor.SetManipulations(result.Default, mods[0].Manipulations); + _editor.SetFiles(result.Default, files, SaveType.None); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps, SaveType.None); + _editor.SetManipulations(result.Default, mods[0].Manipulations, SaveType.None); + _editor.ForceSave(result.Default); } else { @@ -277,6 +279,7 @@ public class ModMerger : IDisposable _editor.SetFiles(result.Default, files); _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); _editor.SetManipulations(result.Default, mods[0].Manipulations); + _editor.ForceSave(result.Default); } else { @@ -285,9 +288,10 @@ public class ModMerger : IDisposable var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group!.Name, option!.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.SetFiles((IModDataContainer)option, files); - _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps); - _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations); + _editor.SetFiles((IModDataContainer)option, files, SaveType.None); + _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps, SaveType.None); + _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations, SaveType.None); + _editor.ForceSave((IModDataContainer)option); } } } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 48d687d0..af5b2d3a 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -83,7 +83,8 @@ public class ItemSwapContainer manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None); manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None); - manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync); + manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.None); + manager.OptionEditor.ForceSave(container, SaveType.ImmediateSync); return true; } catch (Exception e) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index e1db0ccf..55e01015 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -166,6 +166,10 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } + /// Forces a file save of the given container's group. + public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue) + => saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) { From b3f87624949c9c48de7bc847782601577bd86336 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 14:50:07 +0200 Subject: [PATCH 159/865] Fix some crash handler issues --- Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs | 2 ++ Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs | 2 ++ Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index 3446530a..11dc52db 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -55,8 +55,10 @@ internal sealed class AnimationInvocationBuffer : MemoryMappedBuffer, IAnimation accessor.Write(16, characterAddress); var span = GetSpan(accessor, 24, 16); collectionId.TryWriteBytes(span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 40); WriteSpan(characterName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index 4036455d..a48fe846 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -38,8 +38,10 @@ internal sealed class CharacterBaseBuffer : MemoryMappedBuffer, ICharacterBaseBu accessor.Write(12, characterAddress); var span = GetSpan(accessor, 20, 16); collectionId.TryWriteBytes(span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 36); WriteSpan(characterName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index 03f63ba4..ac507e7f 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -44,12 +44,16 @@ internal sealed class ModdedFileBuffer : MemoryMappedBuffer, IModdedFileBufferWr accessor.Write(12, characterAddress); var span = GetSpan(accessor, 20, 16); collectionId.TryWriteBytes(span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 36, 80); WriteSpan(characterName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 116, 260); WriteSpan(requestedFileName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); span = GetSpan(accessor, 376); WriteSpan(actualFileName, span); + accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } From 250c4034e04323b025cd39cd4641be8e04d431ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 14:50:33 +0200 Subject: [PATCH 160/865] Improve root directory behavior and AddMods. --- Penumbra/Api/Api/ModsApi.cs | 4 +++- Penumbra/Mods/Manager/ModManager.cs | 16 +++++++++------- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 548831d5..60b00d37 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -75,7 +75,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable if (!dir.Exists) return ApiHelpers.Return(PenumbraApiEc.FileMissing, args); - if (dir.Parent == null || Path.GetFullPath(_modManager.BasePath.FullName) != Path.GetFullPath(dir.Parent.FullName)) + if (dir.Parent == null + || Path.TrimEndingDirectorySeparator(Path.GetFullPath(_modManager.BasePath.FullName)) + != Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName))) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); _modManager.AddMod(dir); diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 010cad19..62b54865 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -47,15 +47,15 @@ public sealed class ModManager : ModStorage, IDisposable DataEditor = dataEditor; OptionEditor = optionEditor; Creator = creator; - SetBaseDirectory(config.ModDirectory, true); + SetBaseDirectory(config.ModDirectory, true, out _); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModManager); DiscoverMods(); } /// Change the mod base directory and discover available mods. - public void DiscoverMods(string newDir) + public void DiscoverMods(string newDir, out string resultNewDir) { - SetBaseDirectory(newDir, false); + SetBaseDirectory(newDir, false, out resultNewDir); DiscoverMods(); } @@ -264,8 +264,9 @@ public sealed class ModManager : ModStorage, IDisposable /// If its not the first time, check if it is the same directory as before. /// Also checks if the directory is available and tries to create it if it is not. /// - private void SetBaseDirectory(string newPath, bool firstTime) + private void SetBaseDirectory(string newPath, bool firstTime, out string resultNewDir) { + resultNewDir = newPath; if (!firstTime && string.Equals(newPath, _config.ModDirectory, StringComparison.Ordinal)) return; @@ -278,7 +279,7 @@ public sealed class ModManager : ModStorage, IDisposable } else { - var newDir = new DirectoryInfo(newPath); + var newDir = new DirectoryInfo(Path.TrimEndingDirectorySeparator(newPath)); if (!newDir.Exists) try { @@ -290,8 +291,9 @@ public sealed class ModManager : ModStorage, IDisposable Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); } - BasePath = newDir; - Valid = Directory.Exists(newDir.FullName); + BasePath = newDir; + Valid = Directory.Exists(newDir.FullName); + resultNewDir = BasePath.FullName; if (!firstTime && _config.ModDirectory != BasePath.FullName) TriggerModDirectoryChange(BasePath.FullName, Valid); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 9989f90a..0de4f790 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -269,7 +269,7 @@ public class SettingsTab : ITab if (_config.ModDirectory != _newModDirectory && _newModDirectory.Length != 0 && DrawPressEnterWarning(_newModDirectory, _config.ModDirectory, pos, save, selected)) - _modManager.DiscoverMods(_newModDirectory); + _modManager.DiscoverMods(_newModDirectory, out _newModDirectory); } /// Draw the Open Directory and Rediscovery buttons. From d7b60206d77610d2d0ba62ca280e93470698c94e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 14:55:10 +0200 Subject: [PATCH 161/865] Improve meta manipulation handling a bit. --- Penumbra/Api/Api/TemporaryApi.cs | 4 +- Penumbra/Api/TempModManager.cs | 2 +- .../Meta/Manipulations/EqdpManipulation.cs | 17 +-- .../Meta/Manipulations/EqpManipulation.cs | 17 +-- .../Meta/Manipulations/EstManipulation.cs | 15 +- .../Manipulations/GlobalEqpManipulation.cs | 31 +++- .../Meta/Manipulations/GmpManipulation.cs | 15 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 140 ++++++++++++++++++ Penumbra/Meta/Manipulations/Rsp.cs | 28 +++- .../Meta/Manipulations/RspManipulation.cs | 27 ++-- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/IModDataContainer.cs | 6 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 2 +- 14 files changed, 242 insertions(+), 66 deletions(-) create mode 100644 Penumbra/Meta/Manipulations/MetaDictionary.cs diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 38d080cc..09a9b7c4 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -160,7 +160,7 @@ public class TemporaryApi( /// Only returns true if all conversions are successful and distinct. /// private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out HashSet? manips) + [NotNullWhen(true)] out MetaDictionary? manips) { if (manipString.Length == 0) { @@ -174,7 +174,7 @@ public class TemporaryApi( return false; } - manips = new HashSet(manipArray!.Length); + manips = new MetaDictionary(manipArray!.Length); foreach (var manip in manipArray.Where(m => m.Validate())) { if (manips.Add(manip)) diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index aee2b447..cbb07436 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -43,7 +43,7 @@ public class TempModManager : IDisposable => _modsForAllCollections; public RedirectResult Register(string tag, ModCollection? collection, Dictionary dict, - HashSet manips, ModPriority priority) + MetaDictionary manips, ModPriority priority) { var mod = GetOrCreateMod(tag, collection, priority, out var created); Penumbra.Log.Verbose($"{(created ? "Created" : "Changed")} temporary Mod {mod.Name}."); diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs index 2c01ce3f..8c5f27e5 100644 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqdpManipulation.cs @@ -8,11 +8,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqdpManipulation : IMetaManipulation +public readonly struct EqdpManipulation(EqdpIdentifier identifier, EqdpEntry entry) : IMetaManipulation { [JsonIgnore] - public EqdpIdentifier Identifier { get; private init; } - public EqdpEntry Entry { get; private init; } + public EqdpIdentifier Identifier { get; } = identifier; + + public EqdpEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public Gender Gender @@ -31,20 +32,18 @@ public readonly struct EqdpManipulation : IMetaManipulation [JsonConstructor] public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) - { - Identifier = new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)); - Entry = Eqdp.Mask(Slot) & entry; - } + : this(new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)), Eqdp.Mask(slot) & entry) + { } public EqdpManipulation Copy(EqdpManipulation entry) { if (entry.Slot != Slot) { var (bit1, bit2) = entry.Entry.ToBits(entry.Slot); - return new EqdpManipulation(Eqdp.FromSlotAndBits(Slot, bit1, bit2), Slot, Gender, Race, SetId); + return new EqdpManipulation(Identifier, Eqdp.FromSlotAndBits(Slot, bit1, bit2)); } - return new EqdpManipulation(entry.Entry, Slot, Gender, Race, SetId); + return new EqdpManipulation(Identifier, entry.Entry); } public EqdpManipulation Copy(EqdpEntry entry) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs index 3bced096..eef21d12 100644 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/EqpManipulation.cs @@ -9,12 +9,13 @@ using Penumbra.Util; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqpManipulation : IMetaManipulation +public readonly struct EqpManipulation(EqpIdentifier identifier, EqpEntry entry) : IMetaManipulation { - [JsonConverter(typeof(ForceNumericFlagEnumConverter))] - public EqpEntry Entry { get; private init; } + [JsonIgnore] + public EqpIdentifier Identifier { get; } = identifier; - public EqpIdentifier Identifier { get; private init; } + [JsonConverter(typeof(ForceNumericFlagEnumConverter))] + public EqpEntry Entry { get; } = entry; public PrimaryId SetId => Identifier.SetId; @@ -25,13 +26,11 @@ public readonly struct EqpManipulation : IMetaManipulation [JsonConstructor] public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) - { - Identifier = new EqpIdentifier(setId, slot); - Entry = Eqp.Mask(slot) & entry; - } + : this(new EqpIdentifier(setId, slot), Eqp.Mask(slot) & entry) + { } public EqpManipulation Copy(EqpEntry entry) - => new(entry, Slot, SetId); + => new(Identifier, entry); public override string ToString() => $"Eqp - {SetId} - {Slot}"; diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index c3f9792f..09abbaa5 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -8,7 +8,7 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EstManipulation : IMetaManipulation +public readonly struct EstManipulation(EstIdentifier identifier, EstEntry entry) : IMetaManipulation { public static string ToName(EstType type) => type switch @@ -20,8 +20,9 @@ public readonly struct EstManipulation : IMetaManipulation _ => "unk", }; - public EstIdentifier Identifier { get; private init; } - public EstEntry Entry { get; private init; } + [JsonIgnore] + public EstIdentifier Identifier { get; } = identifier; + public EstEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public Gender Gender @@ -41,13 +42,11 @@ public readonly struct EstManipulation : IMetaManipulation [JsonConstructor] public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) - { - Entry = entry; - Identifier = new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)); - } + : this(new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)), entry) + { } public EstManipulation Copy(EstEntry entry) - => new(Gender, Race, Slot, SetId, entry); + => new(Identifier, entry); public override string ToString() diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index ada543dc..94c892e2 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -1,9 +1,11 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly struct GlobalEqpManipulation : IMetaManipulation +public readonly struct GlobalEqpManipulation : IMetaManipulation, IMetaIdentifier { public GlobalEqpType Type { get; init; } public PrimaryId Condition { get; init; } @@ -19,6 +21,28 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation() ?? (GlobalEqpType)100; + var condition = jObj[nameof(Condition)]?.ToObject() ?? 0; + var ret = new GlobalEqpManipulation + { + Type = type, + Condition = condition, + }; + return ret.Validate() ? ret : null; + } + public bool Equals(GlobalEqpManipulation other) => Type == other.Type @@ -45,6 +69,9 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { } + public MetaIndex FileIndex() - => (MetaIndex)(-1); + => MetaIndex.Eqp; } diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs index 0b2a9f4b..431f6325 100644 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GmpManipulation.cs @@ -6,24 +6,23 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct GmpManipulation : IMetaManipulation +public readonly struct GmpManipulation(GmpIdentifier identifier, GmpEntry entry) : IMetaManipulation { - public GmpIdentifier Identifier { get; private init; } + [JsonIgnore] + public GmpIdentifier Identifier { get; } = identifier; - public GmpEntry Entry { get; private init; } + public GmpEntry Entry { get; } = entry; public PrimaryId SetId => Identifier.SetId; [JsonConstructor] public GmpManipulation(GmpEntry entry, PrimaryId setId) - { - Entry = entry; - Identifier = new GmpIdentifier(setId); - } + : this(new GmpIdentifier(setId), entry) + { } public GmpManipulation Copy(GmpEntry entry) - => new(entry, SetId); + => new(Identifier, entry); public override string ToString() => $"Gmp - {SetId}"; diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs new file mode 100644 index 00000000..65252c5d --- /dev/null +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -0,0 +1,140 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; +using ImcEntry = Penumbra.GameData.Structs.ImcEntry; + +namespace Penumbra.Meta.Manipulations; + +[JsonConverter(typeof(Converter))] +public sealed class MetaDictionary : HashSet +{ + public MetaDictionary() + { } + + public MetaDictionary(int capacity) + : base(capacity) + { } + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MetaDictionary? value, JsonSerializer serializer) + { + if (value is null) + { + writer.WriteNull(); + return; + } + + writer.WriteStartArray(); + foreach (var item in value) + { + writer.WriteStartObject(); + writer.WritePropertyName("Type"); + writer.WriteValue(item.ManipulationType.ToString()); + writer.WritePropertyName("Manipulation"); + serializer.Serialize(writer, item.Manipulation); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var dict = existingValue ?? []; + dict.Clear(); + var jObj = JArray.Load(reader); + foreach (var item in jObj) + { + var type = item["Type"]?.ToObject() ?? MetaManipulation.Type.Unknown; + if (type is MetaManipulation.Type.Unknown) + { + Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); + continue; + } + + if (item["Manipulation"] is not JObject manip) + { + Penumbra.Log.Warning($"Manipulation of type {type} does not contain manipulation data."); + continue; + } + + switch (type) + { + case MetaManipulation.Type.Imc: + { + var identifier = ImcIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new ImcManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); + break; + } + case MetaManipulation.Type.Eqdp: + { + var identifier = EqdpIdentifier.FromJson(manip); + var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EqdpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Eqp: + { + var identifier = EqpIdentifier.FromJson(manip); + var entry = (EqpEntry?)manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EqpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Est: + { + var identifier = EstIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new EstManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid EST Manipulation encountered."); + break; + } + case MetaManipulation.Type.Gmp: + { + var identifier = GmpIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new GmpManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); + break; + } + case MetaManipulation.Type.Rsp: + { + var identifier = RspIdentifier.FromJson(manip); + var entry = manip["Entry"]?.ToObject(); + if (identifier.HasValue && entry.HasValue) + dict.Add(new MetaManipulation(new RspManipulation(identifier.Value, entry.Value))); + else + Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); + break; + } + case MetaManipulation.Type.GlobalEqp: + { + var identifier = GlobalEqpManipulation.FromJson(manip); + if (identifier.HasValue) + dict.Add(new MetaManipulation(identifier.Value)); + else + Penumbra.Log.Warning("Invalid Global EQP Manipulation encountered."); + break; + } + } + } + + return dict; + } + } +} diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 29cdfd71..ca7cb1c5 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,3 +1,4 @@ +using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -12,13 +13,31 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() - => throw new NotImplementedException(); + => MetaIndex.HumanCmp; public bool Validate() - => throw new NotImplementedException(); + => SubRace is not SubRace.Unknown + && Enum.IsDefined(SubRace) + && Attribute is not RspAttribute.NumAttributes + && Enum.IsDefined(Attribute); public JObject AddToJson(JObject jObj) - => throw new NotImplementedException(); + { + jObj["SubRace"] = SubRace.ToString(); + jObj["Attribute"] = Attribute.ToString(); + return jObj; + } + + public static RspIdentifier? FromJson(JObject? jObj) + { + if (jObj == null) + return null; + + var subRace = jObj["SubRace"]?.ToObject() ?? SubRace.Unknown; + var attribute = jObj["Attribute"]?.ToObject() ?? RspAttribute.NumAttributes; + var ret = new RspIdentifier(subRace, attribute); + return ret.Validate() ? ret : null; + } } [JsonConverter(typeof(Converter))] @@ -28,6 +47,9 @@ public readonly record struct RspEntry(float Value) : IComparisonOperators Value is >= MinValue and <= MaxValue; + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, RspEntry value, JsonSerializer serializer) diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs index 04691c9f..e2282c41 100644 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ b/Penumbra/Meta/Manipulations/RspManipulation.cs @@ -7,10 +7,12 @@ using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; [StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct RspManipulation : IMetaManipulation +public readonly struct RspManipulation(RspIdentifier identifier, RspEntry entry) : IMetaManipulation { - public RspIdentifier Identifier { get; private init; } - public RspEntry Entry { get; private init; } + [JsonIgnore] + public RspIdentifier Identifier { get; } = identifier; + + public RspEntry Entry { get; } = entry; [JsonConverter(typeof(StringEnumConverter))] public SubRace SubRace @@ -22,13 +24,11 @@ public readonly struct RspManipulation : IMetaManipulation [JsonConstructor] public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) - { - Entry = entry; - Identifier = new RspIdentifier(subRace, attribute); - } + : this(new RspIdentifier(subRace, attribute), entry) + { } public RspManipulation Copy(RspEntry entry) - => new(SubRace, Attribute, entry); + => new(Identifier, entry); public override string ToString() => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; @@ -63,14 +63,5 @@ public readonly struct RspManipulation : IMetaManipulation } public bool Validate() - { - if (SubRace is SubRace.Unknown || !Enum.IsDefined(SubRace)) - return false; - if (!Enum.IsDefined(Attribute)) - return false; - if (Entry.Value is < RspEntry.MinValue or > RspEntry.MaxValue) - return false; - - return true; - } + => Identifier.Validate() && Entry.Validate(); } diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 1a234879..5a300a48 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = []; IMod IModDataContainer.Mod => Mod; diff --git a/Penumbra/Mods/SubMods/IModDataContainer.cs b/Penumbra/Mods/SubMods/IModDataContainer.cs index 7f7ef4a6..1a89ec17 100644 --- a/Penumbra/Mods/SubMods/IModDataContainer.cs +++ b/Penumbra/Mods/SubMods/IModDataContainer.cs @@ -10,9 +10,9 @@ public interface IModDataContainer public IMod Mod { get; } public IModGroup? Group { get; } - public Dictionary Files { get; set; } - public Dictionary FileSwaps { get; set; } - public HashSet Manipulations { get; set; } + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public MetaDictionary Manipulations { get; set; } public string GetName(); public string GetFullName(); diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 02d86af2..378f6dc8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -33,7 +33,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public HashSet Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = []; public void AddDataTo(Dictionary redirections, HashSet manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 6e6e72ab..e0a03c92 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -57,7 +57,7 @@ public class TemporaryMod : IMod public bool SetManipulation(MetaManipulation manip) => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); - public void SetAll(Dictionary dict, HashSet manips) + public void SetAll(Dictionary dict, MetaDictionary manips) { Default.Files = dict; Default.Manipulations = manips; From 94fdd848b718ef2c7e932faff6b108f7ed7287d8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:06:37 +0200 Subject: [PATCH 162/865] Expand on MetaDictionary to use separate dictionaries. --- Penumbra/Api/Api/TemporaryApi.cs | 4 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 244 +++++++++++++++++- Penumbra/Mods/Editor/IMod.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 21 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/Mod.cs | 2 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 2 +- 15 files changed, 257 insertions(+), 36 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 09a9b7c4..995ec388 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -174,8 +174,8 @@ public class TemporaryApi( return false; } - manips = new MetaDictionary(manipArray!.Length); - foreach (var manip in manipArray.Where(m => m.Validate())) + manips = []; + foreach (var manip in manipArray!.Where(m => m.Validate())) { if (manips.Add(manip)) continue; diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 65252c5d..b0b7f011 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,19 +1,237 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Structs; +using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] -public sealed class MetaDictionary : HashSet +public sealed class MetaDictionary : IEnumerable { - public MetaDictionary() - { } + private readonly Dictionary _imc = []; + private readonly Dictionary _eqp = []; + private readonly Dictionary _eqdp = []; + private readonly Dictionary _est = []; + private readonly Dictionary _rsp = []; + private readonly Dictionary _gmp = []; + private readonly HashSet _globalEqp = []; - public MetaDictionary(int capacity) - : base(capacity) - { } + public int Count { get; private set; } + + public void Clear() + { + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _est.Clear(); + _rsp.Clear(); + _gmp.Clear(); + _globalEqp.Clear(); + } + + public IEnumerator GetEnumerator() + => _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) + .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value)))) + .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value)))) + .Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) + .Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) + .Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) + .Concat(_globalEqp.Select(manip => new MetaManipulation(manip))).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public bool Add(MetaManipulation manip) + { + var ret = manip.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.TryAdd(manip.Imc.Identifier, manip.Imc.Entry), + MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, manip.Eqdp.Entry), + MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, manip.Eqp.Entry), + MetaManipulation.Type.Est => _est.TryAdd(manip.Est.Identifier, manip.Est.Entry), + MetaManipulation.Type.Gmp => _gmp.TryAdd(manip.Gmp.Identifier, manip.Gmp.Entry), + MetaManipulation.Type.Rsp => _rsp.TryAdd(manip.Rsp.Identifier, manip.Rsp.Entry), + MetaManipulation.Type.GlobalEqp => _globalEqp.Add(manip.GlobalEqp), + _ => false, + }; + + if (ret) + ++Count; + return ret; + } + + public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) + { + if (!_imc.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(EstIdentifier identifier, EstEntry entry) + { + if (!_est.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(GmpIdentifier identifier, GmpEntry entry) + { + if (!_gmp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(RspIdentifier identifier, RspEntry entry) + { + if (!_rsp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + public bool TryAdd(GlobalEqpManipulation identifier) + { + if (!_globalEqp.Add(identifier)) + return false; + + ++Count; + return true; + } + + public bool Remove(MetaManipulation manip) + { + var ret = manip.ManipulationType switch + { + MetaManipulation.Type.Imc => _imc.Remove(manip.Imc.Identifier), + MetaManipulation.Type.Eqdp => _eqdp.Remove(manip.Eqdp.Identifier), + MetaManipulation.Type.Eqp => _eqp.Remove(manip.Eqp.Identifier), + MetaManipulation.Type.Est => _est.Remove(manip.Est.Identifier), + MetaManipulation.Type.Gmp => _gmp.Remove(manip.Gmp.Identifier), + MetaManipulation.Type.Rsp => _rsp.Remove(manip.Rsp.Identifier), + MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(manip.GlobalEqp), + _ => false, + }; + if (ret) + --Count; + return ret; + } + + public void UnionWith(IEnumerable manips) + { + foreach (var manip in manips) + Add(manip); + } + + public bool TryGetValue(MetaManipulation identifier, out MetaManipulation oldValue) + { + switch (identifier.ManipulationType) + { + case MetaManipulation.Type.Imc: + if (_imc.TryGetValue(identifier.Imc.Identifier, out var oldImc)) + { + oldValue = new MetaManipulation(new ImcManipulation(identifier.Imc.Identifier, oldImc)); + return true; + } + + break; + case MetaManipulation.Type.Eqdp: + if (_eqp.TryGetValue(identifier.Eqp.Identifier, out var oldEqdp)) + { + oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp)); + return true; + } + + break; + case MetaManipulation.Type.Eqp: + if (_eqdp.TryGetValue(identifier.Eqdp.Identifier, out var oldEqp)) + { + oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp)); + return true; + } + + break; + case MetaManipulation.Type.Est: + if (_est.TryGetValue(identifier.Est.Identifier, out var oldEst)) + { + oldValue = new MetaManipulation(new EstManipulation(identifier.Est.Identifier, oldEst)); + return true; + } + + break; + case MetaManipulation.Type.Gmp: + if (_gmp.TryGetValue(identifier.Gmp.Identifier, out var oldGmp)) + { + oldValue = new MetaManipulation(new GmpManipulation(identifier.Gmp.Identifier, oldGmp)); + return true; + } + + break; + case MetaManipulation.Type.Rsp: + if (_rsp.TryGetValue(identifier.Rsp.Identifier, out var oldRsp)) + { + oldValue = new MetaManipulation(new RspManipulation(identifier.Rsp.Identifier, oldRsp)); + return true; + } + + break; + case MetaManipulation.Type.GlobalEqp: + if (_globalEqp.TryGetValue(identifier.GlobalEqp, out var oldGlobalEqp)) + { + oldValue = new MetaManipulation(oldGlobalEqp); + return true; + } + + break; + } + + oldValue = default; + return false; + } + + public void SetTo(MetaDictionary other) + { + _imc.SetTo(other._imc); + _eqp.SetTo(other._eqp); + _eqdp.SetTo(other._eqdp); + _est.SetTo(other._est); + _rsp.SetTo(other._rsp); + _gmp.SetTo(other._gmp); + _globalEqp.SetTo(other._globalEqp); + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + } + + public MetaDictionary Clone() + { + var ret = new MetaDictionary(); + ret.SetTo(this); + return ret; + } private class Converter : JsonConverter { @@ -67,7 +285,7 @@ public sealed class MetaDictionary : HashSet var identifier = ImcIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new ImcManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); break; @@ -77,7 +295,7 @@ public sealed class MetaDictionary : HashSet var identifier = EqdpIdentifier.FromJson(manip); var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new EqdpManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); break; @@ -87,7 +305,7 @@ public sealed class MetaDictionary : HashSet var identifier = EqpIdentifier.FromJson(manip); var entry = (EqpEntry?)manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new EqpManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); break; @@ -97,7 +315,7 @@ public sealed class MetaDictionary : HashSet var identifier = EstIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new EstManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid EST Manipulation encountered."); break; @@ -107,7 +325,7 @@ public sealed class MetaDictionary : HashSet var identifier = GmpIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new GmpManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); break; @@ -117,7 +335,7 @@ public sealed class MetaDictionary : HashSet var identifier = RspIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); if (identifier.HasValue && entry.HasValue) - dict.Add(new MetaManipulation(new RspManipulation(identifier.Value, entry.Value))); + dict.TryAdd(identifier.Value, entry.Value); else Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; @@ -126,7 +344,7 @@ public sealed class MetaDictionary : HashSet { var identifier = GlobalEqpManipulation.FromJson(manip); if (identifier.HasValue) - dict.Add(new MetaManipulation(identifier.Value)); + dict.TryAdd(identifier.Value); else Penumbra.Log.Warning("Invalid Global EQP Manipulation encountered."); break; diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index d4c881e9..06c31846 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -8,7 +8,7 @@ namespace Penumbra.Mods.Editor; public record struct AppliedModData( Dictionary FileRedirections, - HashSet Manipulations) + MetaDictionary Manipulations) { public static readonly AppliedModData Empty = new([], []); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index f5fc9cd7..4faced80 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -158,7 +158,7 @@ public class ModMerger : IDisposable { var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var manips = option.Manipulations.ToHashSet(); + var manips = option.Manipulations.Clone(); foreach (var originalOption in mergeOptions) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 86853755..45d9f8a1 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -145,7 +145,7 @@ public class ModMetaEditor(ModManager modManager) if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet()); + modManager.OptionEditor.SetManipulations(container, [..Recombine()]); Changes = false; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index fcc8c093..00f47e25 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -43,7 +43,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index b336203d..383bc9fd 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -99,7 +99,7 @@ public class ImcModGroup(Mod mod) : IModGroup => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 7816d628..220d0a7c 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -116,7 +116,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new MultiModGroupEditDrawer(editDrawer, this); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) { diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index a6ebd846..a559d609 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -101,7 +101,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new SingleModGroupEditDrawer(editDrawer, this); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (OptionData.Count == 0) return; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index af5b2d3a..67a5d007 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -23,7 +23,7 @@ public class ItemSwapContainer public IReadOnlyDictionary ModRedirections => _appliedModData.FileRedirections; - public IReadOnlySet ModManipulations + public MetaDictionary ModManipulations => _appliedModData.Manipulations; public readonly List Swaps = []; @@ -42,9 +42,10 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null) + public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, + DirectoryInfo? directory = null) { - var convertedManips = new HashSet(Swaps.Count); + var convertedManips = new MetaDictionary(); var convertedFiles = new Dictionary(Swaps.Count); var convertedSwaps = new Dictionary(Swaps.Count); directory ??= mod.ModPath; @@ -98,13 +99,9 @@ public class ItemSwapContainer { Clear(); if (mod == null || mod.Index < 0) - { - _appliedModData = AppliedModData.Empty; - } + _appliedModData = AppliedModData.Empty; else - { _appliedModData = ModSettings.GetResolveData(mod, settings); - } } public ItemSwapContainer(MetaFileManager manager, ObjectIdentification identifier) @@ -121,7 +118,13 @@ public class ItemSwapContainer private Func MetaResolver(ModCollection? collection) { - var set = collection?.MetaCache?.Manipulations.ToHashSet() ?? _appliedModData.Manipulations; + if (collection?.MetaCache?.Manipulations is { } cache) + { + MetaDictionary dict = [.. cache]; + return m => dict.TryGetValue(m, out var a) ? a : m; + } + + var set = _appliedModData.Manipulations; return m => set.TryGetValue(m, out var a) ? a : m; } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 55e01015..01092862 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -142,7 +142,7 @@ public class ModGroupEditor( } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void SetManipulations(IModDataContainer subMod, HashSet manipulations, SaveType saveType = SaveType.Queue) + public void SetManipulations(IModDataContainer subMod, MetaDictionary manipulations, SaveType saveType = SaveType.Queue) { if (subMod.Manipulations.Count == manipulations.Count && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 783ef3e6..a7f87dcd 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -71,7 +71,7 @@ public sealed class Mod : IMod return AppliedModData.Empty; var dictRedirections = new Dictionary(TotalFileCount); - var setManips = new HashSet(TotalManipulations); + var setManips = new MetaDictionary(); foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) { var config = settings.Settings[groupIndex]; diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index 5a300a48..dcd33610 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -21,7 +21,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer IModGroup? IModDataContainer.Group => null; - public void AddTo(Dictionary redirections, HashSet manipulations) + public void AddTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 378f6dc8..ed7b6ff8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -35,7 +35,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary FileSwaps { get; set; } = []; public MetaDictionary Manipulations { get; set; } = []; - public void AddDataTo(Dictionary redirections, HashSet manipulations) + public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); public string GetName() diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index b984b570..06a924c8 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -21,7 +21,7 @@ public static class SubMod /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void AddContainerTo(IModDataContainer container, Dictionary redirections, - HashSet manipulations) + MetaDictionary manipulations) { foreach (var (path, file) in container.Files) redirections.TryAdd(path, file); From 13156a58e92ab64965879f994eb9f1aec77d8f12 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:09:34 +0200 Subject: [PATCH 163/865] Remove unused functions. --- Penumbra/Mods/TemporaryMod.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e0a03c92..a715f786 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -51,12 +51,6 @@ public class TemporaryMod : IMod public TemporaryMod() => Default = new DefaultSubMod(this); - public void SetFile(Utf8GamePath gamePath, FullPath fullPath) - => Default.Files[gamePath] = fullPath; - - public bool SetManipulation(MetaManipulation manip) - => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); - public void SetAll(Dictionary dict, MetaDictionary manips) { Default.Files = dict; From e0339160e908276a97963fd498f9e785a88ee690 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:25:08 +0200 Subject: [PATCH 164/865] Start removing MetaManipulation functions. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 40 +++++++++---------- .../Manager/OptionEditor/ModGroupEditor.cs | 3 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index b0b7f011..b9d7990d 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -124,28 +124,28 @@ public sealed class MetaDictionary : IEnumerable return true; } - public bool Remove(MetaManipulation manip) + public void UnionWith(MetaDictionary manips) { - var ret = manip.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Remove(manip.Imc.Identifier), - MetaManipulation.Type.Eqdp => _eqdp.Remove(manip.Eqdp.Identifier), - MetaManipulation.Type.Eqp => _eqp.Remove(manip.Eqp.Identifier), - MetaManipulation.Type.Est => _est.Remove(manip.Est.Identifier), - MetaManipulation.Type.Gmp => _gmp.Remove(manip.Gmp.Identifier), - MetaManipulation.Type.Rsp => _rsp.Remove(manip.Rsp.Identifier), - MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(manip.GlobalEqp), - _ => false, - }; - if (ret) - --Count; - return ret; - } + foreach (var (identifier, entry) in manips._imc) + TryAdd(identifier, entry); - public void UnionWith(IEnumerable manips) - { - foreach (var manip in manips) - Add(manip); + foreach (var (identifier, entry) in manips._eqp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._eqdp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._gmp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._rsp) + TryAdd(identifier, entry); + + foreach (var (identifier, entry) in manips._est) + TryAdd(identifier, entry); + + foreach (var identifier in manips._globalEqp) + TryAdd(identifier); } public bool TryGetValue(MetaManipulation identifier, out MetaManipulation oldValue) diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 01092862..594ec9d2 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -144,8 +144,7 @@ public class ModGroupEditor( /// Set the meta manipulations for a given option. Replaces existing manipulations. public void SetManipulations(IModDataContainer subMod, MetaDictionary manipulations, SaveType saveType = SaveType.Queue) { - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + if (subMod.Manipulations.Equals(manipulations)) return; communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); From 5ca9e63a2a5f1f44dd9c5859578c3684bc6df4e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 16:34:51 +0200 Subject: [PATCH 165/865] Use internal entries. --- Penumbra/Meta/Manipulations/Eqdp.cs | 6 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 70 ++++++++++++------- Penumbra/Mods/ModCreator.cs | 4 +- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 6d6942e6..a986d475 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -71,13 +71,13 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge } } -public readonly record struct InternalEqdpEntry(bool Model, bool Material) +public readonly record struct EqdpEntryInternal(bool Model, bool Material) { - private InternalEqdpEntry((bool, bool) val) + private EqdpEntryInternal((bool, bool) val) : this(val.Item1, val.Item2) { } - public InternalEqdpEntry(EqdpEntry entry, EquipSlot slot) + public EqdpEntryInternal(EqdpEntry entry, EquipSlot slot) : this(entry.ToBits(slot)) { } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index b9d7990d..941cdf34 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -9,13 +9,13 @@ namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] public sealed class MetaDictionary : IEnumerable { - private readonly Dictionary _imc = []; - private readonly Dictionary _eqp = []; - private readonly Dictionary _eqdp = []; - private readonly Dictionary _est = []; - private readonly Dictionary _rsp = []; - private readonly Dictionary _gmp = []; - private readonly HashSet _globalEqp = []; + private readonly Dictionary _imc = []; + private readonly Dictionary _eqp = []; + private readonly Dictionary _eqdp = []; + private readonly Dictionary _est = []; + private readonly Dictionary _rsp = []; + private readonly Dictionary _gmp = []; + private readonly HashSet _globalEqp = []; public int Count { get; private set; } @@ -30,10 +30,20 @@ public sealed class MetaDictionary : IEnumerable _globalEqp.Clear(); } + public bool Equals(MetaDictionary other) + => Count == other.Count + && _imc.SetEquals(other._imc) + && _eqp.SetEquals(other._eqp) + && _eqdp.SetEquals(other._eqdp) + && _est.SetEquals(other._est) + && _rsp.SetEquals(other._rsp) + && _gmp.SetEquals(other._gmp) + && _globalEqp.SetEquals(other._globalEqp); + public IEnumerator GetEnumerator() => _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) - .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value)))) - .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value)))) + .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) + .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) .Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) .Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) .Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) @@ -47,8 +57,8 @@ public sealed class MetaDictionary : IEnumerable var ret = manip.ManipulationType switch { MetaManipulation.Type.Imc => _imc.TryAdd(manip.Imc.Identifier, manip.Imc.Entry), - MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, manip.Eqdp.Entry), - MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, manip.Eqp.Entry), + MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, new EqdpEntryInternal(manip.Eqdp.Entry, manip.Eqdp.Slot)), + MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, new EqpEntryInternal(manip.Eqp.Entry, manip.Eqp.Slot)), MetaManipulation.Type.Est => _est.TryAdd(manip.Est.Identifier, manip.Est.Entry), MetaManipulation.Type.Gmp => _gmp.TryAdd(manip.Gmp.Identifier, manip.Gmp.Entry), MetaManipulation.Type.Rsp => _rsp.TryAdd(manip.Rsp.Identifier, manip.Rsp.Entry), @@ -71,22 +81,10 @@ public sealed class MetaDictionary : IEnumerable } public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) - { - if (!_eqp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } + => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) - { - if (!_eqdp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } + => TryAdd(identifier, new EqdpEntryInternal(entry, identifier.Slot)); public bool TryAdd(EstIdentifier identifier, EstEntry entry) { @@ -163,7 +161,7 @@ public sealed class MetaDictionary : IEnumerable case MetaManipulation.Type.Eqdp: if (_eqp.TryGetValue(identifier.Eqp.Identifier, out var oldEqdp)) { - oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp)); + oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp.ToEntry(identifier.Eqp.Slot))); return true; } @@ -171,7 +169,7 @@ public sealed class MetaDictionary : IEnumerable case MetaManipulation.Type.Eqp: if (_eqdp.TryGetValue(identifier.Eqdp.Identifier, out var oldEqp)) { - oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp)); + oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp.ToEntry(identifier.Eqdp.Slot))); return true; } @@ -355,4 +353,22 @@ public sealed class MetaDictionary : IEnumerable return dict; } } + + private bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + + private bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index ed4245c4..0035fd41 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -198,7 +198,7 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith(meta.MetaManipulations); + option.Manipulations.UnionWith([.. meta.MetaManipulations]); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { @@ -212,7 +212,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith(rgsp.MetaManipulations); + option.Manipulations.UnionWith([.. rgsp.MetaManipulations]); } } catch (Exception e) From 0445ed0ef9ed456a45ac100007f1e847ecd2e68e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 19:09:46 +0200 Subject: [PATCH 166/865] Remove TryGetValue(MetaManipulation) from MetaDictionary. --- Penumbra/Meta/Files/EqdpFile.cs | 4 + Penumbra/Meta/Files/EqpGmpFile.cs | 4 + Penumbra/Meta/Files/EstFile.cs | 3 + Penumbra/Meta/Files/ImcFile.cs | 3 + Penumbra/Meta/Manipulations/Eqdp.cs | 5 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 85 ++++-------- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 131 +++++++++--------- Penumbra/Mods/ItemSwap/ItemSwap.cs | 21 ++- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 23 ++- Penumbra/Mods/ItemSwap/Swaps.cs | 82 +++++++---- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- 11 files changed, 182 insertions(+), 183 deletions(-) diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index c76c4efd..e46e82e9 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -2,6 +2,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -126,4 +127,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, PrimaryId primaryId) => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], primaryId); + + public static EqdpEntry GetDefault(MetaFileManager manager, EqdpIdentifier identifier) + => GetDefault(manager, CharacterUtility.ReverseIndices[(int)identifier.FileIndex()], identifier.SetId); } diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 70067c2b..17541c4f 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -1,6 +1,7 @@ using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; +using Penumbra.Meta.Manipulations; using Penumbra.String.Functions; namespace Penumbra.Meta.Files; @@ -164,6 +165,9 @@ public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable public static GmpEntry GetDefault(MetaFileManager manager, PrimaryId primaryIdx) => new() { Value = GetDefaultInternal(manager, InternalIndex, primaryIdx, GmpEntry.Default.Value) }; + public static GmpEntry GetDefault(MetaFileManager manager, GmpIdentifier identifier) + => new() { Value = GetDefaultInternal(manager, InternalIndex, identifier.SetId, GmpEntry.Default.Value) }; + public void Reset(IEnumerable entries) { foreach (var entry in entries) diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index ee38ea1e..f3860416 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -184,4 +184,7 @@ public sealed unsafe class EstFile : MetaBaseFile public static EstEntry GetDefault(MetaFileManager manager, EstType estType, GenderRace genderRace, PrimaryId primaryId) => GetDefault(manager, (MetaIndex)estType, genderRace, primaryId); + + public static EstEntry GetDefault(MetaFileManager manager, EstIdentifier identifier) + => GetDefault(manager, identifier.FileIndex(), identifier.GenderRace, identifier.SetId); } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 5d704cf8..892f5b44 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -65,6 +65,9 @@ public unsafe class ImcFile : MetaBaseFile return ptr == null ? new ImcEntry() : *ptr; } + public ImcEntry GetEntry(EquipSlot slot, Variant variantIdx) + => GetEntry(PartIndex(slot), variantIdx); + public ImcEntry GetEntry(int partIdx, Variant variantIdx, out bool exists) { var ptr = VariantPtr(Data, partIdx, variantIdx); diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index a986d475..6306f419 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -71,7 +71,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge } } -public readonly record struct EqdpEntryInternal(bool Model, bool Material) +public readonly record struct EqdpEntryInternal(bool Material, bool Model) { private EqdpEntryInternal((bool, bool) val) : this(val.Item1, val.Item2) @@ -81,7 +81,6 @@ public readonly record struct EqdpEntryInternal(bool Model, bool Material) : this(entry.ToBits(slot)) { } - public EqdpEntry ToEntry(EquipSlot slot) - => Eqdp.FromSlotAndBits(slot, Model, Material); + => Eqdp.FromSlotAndBits(slot, Material, Model); } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 941cdf34..51149e3b 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -52,6 +52,19 @@ public sealed class MetaDictionary : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public bool Add(IMetaIdentifier identifier, object entry) + => identifier switch + { + EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && Add(eqdpIdentifier, e), + EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && Add(eqpIdentifier, e), + EstIdentifier estIdentifier => entry is EstEntry e && Add(estIdentifier, e), + GlobalEqpManipulation globalEqpManipulation => Add(globalEqpManipulation), + GmpIdentifier gmpIdentifier => entry is GmpEntry e && Add(gmpIdentifier, e), + ImcIdentifier imcIdentifier => entry is ImcEntry e && Add(imcIdentifier, e), + RspIdentifier rspIdentifier => entry is RspEntry e && Add(rspIdentifier, e), + _ => false, + }; + public bool Add(MetaManipulation manip) { var ret = manip.ManipulationType switch @@ -146,71 +159,23 @@ public sealed class MetaDictionary : IEnumerable TryAdd(identifier); } - public bool TryGetValue(MetaManipulation identifier, out MetaManipulation oldValue) - { - switch (identifier.ManipulationType) - { - case MetaManipulation.Type.Imc: - if (_imc.TryGetValue(identifier.Imc.Identifier, out var oldImc)) - { - oldValue = new MetaManipulation(new ImcManipulation(identifier.Imc.Identifier, oldImc)); - return true; - } + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) + => _est.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Eqdp: - if (_eqp.TryGetValue(identifier.Eqp.Identifier, out var oldEqdp)) - { - oldValue = new MetaManipulation(new EqpManipulation(identifier.Eqp.Identifier, oldEqdp.ToEntry(identifier.Eqp.Slot))); - return true; - } + public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) + => _eqp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Eqp: - if (_eqdp.TryGetValue(identifier.Eqdp.Identifier, out var oldEqp)) - { - oldValue = new MetaManipulation(new EqdpManipulation(identifier.Eqdp.Identifier, oldEqp.ToEntry(identifier.Eqdp.Slot))); - return true; - } + public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) + => _eqdp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Est: - if (_est.TryGetValue(identifier.Est.Identifier, out var oldEst)) - { - oldValue = new MetaManipulation(new EstManipulation(identifier.Est.Identifier, oldEst)); - return true; - } + public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) + => _gmp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Gmp: - if (_gmp.TryGetValue(identifier.Gmp.Identifier, out var oldGmp)) - { - oldValue = new MetaManipulation(new GmpManipulation(identifier.Gmp.Identifier, oldGmp)); - return true; - } + public bool TryGetValue(RspIdentifier identifier, out RspEntry value) + => _rsp.TryGetValue(identifier, out value); - break; - case MetaManipulation.Type.Rsp: - if (_rsp.TryGetValue(identifier.Rsp.Identifier, out var oldRsp)) - { - oldValue = new MetaManipulation(new RspManipulation(identifier.Rsp.Identifier, oldRsp)); - return true; - } - - break; - case MetaManipulation.Type.GlobalEqp: - if (_globalEqp.TryGetValue(identifier.GlobalEqp, out var oldGlobalEqp)) - { - oldValue = new MetaManipulation(oldGlobalEqp); - return true; - } - - break; - } - - oldValue = default; - return false; - } + public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) + => _imc.TryGetValue(identifier, out value); public void SetTo(MetaDictionary other) { diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 3efee857..e42a1d31 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -1,4 +1,4 @@ -using Penumbra.Api.Enums; +using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -16,32 +16,19 @@ public static class EquipmentSwap private static EquipSlot[] ConvertSlots(EquipSlot slot, bool rFinger, bool lFinger) { if (slot != EquipSlot.RFinger) - return new[] - { - slot, - }; + return [slot]; return rFinger ? lFinger - ? new[] - { - EquipSlot.RFinger, - EquipSlot.LFinger, - } - : new[] - { - EquipSlot.RFinger, - } + ? [EquipSlot.RFinger, EquipSlot.LFinger] + : [EquipSlot.RFinger] : lFinger - ? new[] - { - EquipSlot.LFinger, - } - : Array.Empty(); + ? [EquipSlot.LFinger] + : []; } public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, - Func redirections, Func manips, + Func redirections, MetaDictionary manips, EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); @@ -50,11 +37,14 @@ public static class EquipmentSwap throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slotTo, variantTo.Id, idTo.Id, default); - var imcFileTo = new ImcFile(manager, imcManip.Identifier); + var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) + ? entry + : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); + var mtrlVariantTo = imcEntry.MaterialId; var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo.Id))).Imc.Entry.MaterialId; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -99,7 +89,7 @@ public static class EquipmentSwap } public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, - Func redirections, Func manips, EquipItem itemFrom, + Func redirections, MetaDictionary manips, EquipItem itemFrom, EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. @@ -120,8 +110,12 @@ public static class EquipmentSwap foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); - var imcManip = new ImcManipulation(slot, variantTo.Id, idTo, default); - var imcFileTo = new ImcFile(manager, imcManip.Identifier); + var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) + ? entry + : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); + var mtrlVariantTo = imcEntry.MaterialId; var isAccessory = slot.IsAccessory(); var estType = slot switch @@ -133,7 +127,6 @@ public static class EquipmentSwap var skipFemale = false; var skipMale = false; - var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -154,7 +147,7 @@ public static class EquipmentSwap if (eqdp != null) swaps.Add(eqdp); - var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false; + var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); @@ -184,22 +177,22 @@ public static class EquipmentSwap return affectedItems; } - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, - PrimaryId idTo, byte mtrlTo) + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) { - var (gender, race) = gr.Split(); - var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom), slotFrom, gender, - race, idFrom); - var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo), slotTo, gender, race, - idTo); - var meta = new MetaSwap(manips, eqdpFrom, eqdpTo); - var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits(slotFrom); + var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpToIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, + eqdpFromDefault, eqdpToIdentifier, + eqdpToDefault); + var (ownMtrl, ownMdl) = meta.SwapToModdedEntry; if (ownMdl) { var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo); @@ -270,38 +263,39 @@ public static class EquipmentSwap return (imc, variants, items); } - public static MetaSwap? CreateGmp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, - PrimaryId idTo) + public static MetaSwap? CreateGmp(MetaFileManager manager, MetaDictionary manips, + EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) { if (slot is not EquipSlot.Head) return null; - var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom), idFrom); - var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo), idTo); - return new MetaSwap(manips, manipFrom, manipTo); + var manipFromIdentifier = new GmpIdentifier(idFrom); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, - Func manips, EquipSlot slot, - PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slot, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, + ImcFile imcFileFrom, ImcFile imcFileTo) => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo); - public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, - Func manips, - EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, + public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, + MetaDictionary manips, EquipSlot slotFrom, EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { - var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); - var manipulationFrom = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, entryFrom); - var manipulationTo = new ImcManipulation(slotTo, variantTo.Id, idTo, entryTo); - var imc = new MetaSwap(manips, manipulationFrom, manipulationTo); + var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); - var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId); + var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId); if (decal != null) imc.ChildSwaps.Add(decal); - var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId); + var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModdedEntry.VfxId); if (avfx != null) imc.ChildSwaps.Add(avfx); @@ -322,7 +316,8 @@ public static class EquipmentSwap // Example: Abyssos Helm / Body - public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, byte vfxId) + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, + byte vfxId) { if (vfxId == 0) return null; @@ -340,17 +335,18 @@ public static class EquipmentSwap return avfx; } - public static MetaSwap? CreateEqp(MetaFileManager manager, Func manips, EquipSlot slot, PrimaryId idFrom, - PrimaryId idTo) + public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, + EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) { if (slot.IsAccessory()) return null; - var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom); - var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo); - var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom); - var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom); - return new MetaSwap(manips, eqpFrom, eqpTo); + var manipFromIdentifier = new EqpIdentifier(idFrom, slot); + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, + manipFromDefault, manipToIdentifier, manipToDefault); } public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, PrimaryId idFrom, @@ -397,7 +393,8 @@ public static class EquipmentSwap return mtrl; } - public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, PrimaryId idTo, + public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, + PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 7fac52c1..efd8080c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -147,24 +147,23 @@ public static class ItemSwap return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } - /// metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. - public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, - Func manips, EstType type, - GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) + public static MetaSwap? CreateEst(MetaFileManager manager, Func redirections, + MetaDictionary manips, EstType type, GenderRace genderRace, PrimaryId idFrom, PrimaryId idTo, bool ownMdl) { if (type == 0) return null; - var (gender, race) = genderRace.Split(); - var fromDefault = new EstManipulation(gender, race, type, idFrom, EstFile.GetDefault(manager, type, genderRace, idFrom)); - var toDefault = new EstManipulation(gender, race, type, idTo, EstFile.GetDefault(manager, type, genderRace, idTo)); - var est = new MetaSwap(manips, fromDefault, toDefault); + var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace); + var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); + var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); + var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); - if (ownMdl && est.SwapApplied.Est.Entry.Value >= 2) + if (ownMdl && est.SwapToModdedEntry.Value >= 2) { - var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + var phyb = CreatePhyb(manager, redirections, type, genderRace, est.SwapToModdedEntry); est.ChildSwaps.Add(phyb); - var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapApplied.Est.Entry); + var sklb = CreateSklb(manager, redirections, type, genderRace, est.SwapToModdedEntry); est.ChildSwaps.Add(sklb); } else if (est.SwapAppliedIsDefault) diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 67a5d007..021ee665 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -74,9 +74,9 @@ public class ItemSwapContainer } break; - case MetaSwap meta: + case IMetaSwap meta: if (!meta.SwapAppliedIsDefault) - convertedManips.Add(meta.SwapApplied); + convertedManips.Add(meta.SwapFromIdentifier, meta.SwapToModdedEntry); break; } @@ -116,17 +116,10 @@ public class ItemSwapContainer ? p => collection.ResolvePath(p) ?? new FullPath(p) : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p); - private Func MetaResolver(ModCollection? collection) - { - if (collection?.MetaCache?.Manipulations is { } cache) - { - MetaDictionary dict = [.. cache]; - return m => dict.TryGetValue(m, out var a) ? a : m; - } - - var set = _appliedModData.Manipulations; - return m => set.TryGetValue(m, out var a) ? a : m; - } + private MetaDictionary MetaResolver(ModCollection? collection) + => collection?.MetaCache?.Manipulations is { } cache + ? [.. cache] + : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true) @@ -161,8 +154,8 @@ public class ItemSwapContainer _ => (EstType)0, }; - var metaResolver = MetaResolver(collection); - var est = ItemSwap.CreateEst(manager, pathResolver, metaResolver, type, race, from, to, true); + var estResolver = MetaResolver(collection); + var est = ItemSwap.CreateEst(manager, pathResolver, estResolver, type, race, from, to, true); Swaps.Add(mdl); if (est != null) diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 27935ffb..36c54203 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -1,58 +1,91 @@ -using Penumbra.Api.Enums; +using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Meta; using static Penumbra.Mods.ItemSwap.ItemSwap; -using Penumbra.Services; namespace Penumbra.Mods.ItemSwap; public class Swap { /// Any further swaps belonging specifically to this tree of changes. - public readonly List ChildSwaps = new(); + public readonly List ChildSwaps = []; public IEnumerable WithChildren() => ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this); } -public sealed class MetaSwap : Swap +public interface IMetaSwap { + public IMetaIdentifier SwapFromIdentifier { get; } + public IMetaIdentifier SwapToIdentifier { get; } + + public object SwapFromDefaultEntry { get; } + public object SwapToDefaultEntry { get; } + public object SwapToModdedEntry { get; } + + public bool SwapToIsDefault { get; } + public bool SwapAppliedIsDefault { get; } +} + +public sealed class MetaSwap : Swap, IMetaSwap + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged, IEquatable +{ + public TIdentifier SwapFromIdentifier; + public TIdentifier SwapToIdentifier; + /// The default value of a specific meta manipulation that needs to be redirected. - public MetaManipulation SwapFrom; + public TEntry SwapFromDefaultEntry; /// The default value of the same Meta entry of the redirected item. - public MetaManipulation SwapToDefault; + public TEntry SwapToDefaultEntry; /// The modded value of the same Meta entry of the redirected item, or the same as SwapToDefault if unmodded. - public MetaManipulation SwapToModded; + public TEntry SwapToModdedEntry; - /// The modded value applied to the specific meta manipulation target before redirection. - public MetaManipulation SwapApplied; - - /// Whether SwapToModded equals SwapToDefault. - public bool SwapToIsDefault; + /// Whether SwapToModdedEntry equals SwapToDefaultEntry. + public bool SwapToIsDefault { get; } /// Whether the applied meta manipulation does not change anything against the default. - public bool SwapAppliedIsDefault; + public bool SwapAppliedIsDefault { get; } /// /// Create a new MetaSwap from the original meta identifier and the target meta identifier. /// - /// A function that converts the given manipulation to the modded one. - /// The original meta identifier with its default value. - /// The target meta identifier with its default value. - public MetaSwap(Func manipulations, MetaManipulation manipFrom, MetaManipulation manipTo) + /// A function that obtains a modded meta entry if it exists. + /// The original meta identifier. + /// The default value for the original meta identifier. + /// The target meta identifier. + /// The default value for the target meta identifier. + public MetaSwap(Func manipulations, TIdentifier manipFromIdentifier, TEntry manipFromEntry, + TIdentifier manipToIdentifier, TEntry manipToEntry) { - SwapFrom = manipFrom; - SwapToDefault = manipTo; + SwapFromIdentifier = manipFromIdentifier; + SwapToIdentifier = manipToIdentifier; + SwapFromDefaultEntry = manipFromEntry; + SwapToDefaultEntry = manipToEntry; - SwapToModded = manipulations(manipTo); - SwapToIsDefault = manipTo.EntryEquals(SwapToModded); - SwapApplied = SwapFrom.WithEntryOf(SwapToModded); - SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom); + SwapToModdedEntry = manipulations(SwapToIdentifier) ?? SwapToDefaultEntry; + SwapToIsDefault = SwapToModdedEntry.Equals(SwapToDefaultEntry); + SwapAppliedIsDefault = SwapToModdedEntry.Equals(SwapFromDefaultEntry); } + + IMetaIdentifier IMetaSwap.SwapFromIdentifier + => SwapFromIdentifier; + + IMetaIdentifier IMetaSwap.SwapToIdentifier + => SwapToIdentifier; + + object IMetaSwap.SwapFromDefaultEntry + => SwapFromDefaultEntry; + + object IMetaSwap.SwapToDefaultEntry + => SwapToDefaultEntry; + + object IMetaSwap.SwapToModdedEntry + => SwapToModdedEntry; } public sealed class FileSwap : Swap @@ -113,8 +146,7 @@ public sealed class FileSwap : Swap /// A full swap container with the actual file in memory. /// True if everything could be read correctly, false otherwise. public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func redirections, - string swapFromRequest, string swapToRequest, - string? swapFromPreChange = null) + string swapFromRequest, string swapToRequest, string? swapFromPreChange = null) { var swap = new FileSwap { diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6010cdaf..62115dd6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -240,7 +240,7 @@ public class ItemSwapTab : IDisposable, ITab { return swap switch { - MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", + IMetaSwap meta => $"{meta.SwapFromIdentifier}: {meta.SwapFromDefaultEntry} -> {meta.SwapToModdedEntry}", FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}", _ => string.Empty, @@ -410,7 +410,7 @@ public class ItemSwapTab : IDisposable, ITab private ImRaii.IEndObject DrawTab(SwapType newTab) { - using var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); + var tab = ImRaii.TabItem(newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString()); if (tab) { _dirty |= _lastTab != newTab; From d9b63320f07b5600bdd71b3420fd740e0f44e6d4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 20:46:29 +0200 Subject: [PATCH 167/865] Some small fixes, parse directly into MetaDictionary. --- Penumbra/Api/Api/TemporaryApi.cs | 24 ++++--------------- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 3 ++- Penumbra/Meta/Manipulations/MetaDictionary.cs | 16 ++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 995ec388..49958a0d 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -159,8 +159,7 @@ public class TemporaryApi( /// The empty string is treated as an empty set. /// Only returns true if all conversions are successful and distinct. /// - private static bool ConvertManips(string manipString, - [NotNullWhen(true)] out MetaDictionary? manips) + private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) { if (manipString.Length == 0) { @@ -168,23 +167,10 @@ public class TemporaryApi( return true; } - if (Functions.FromCompressedBase64(manipString, out var manipArray) != MetaManipulation.CurrentVersion) - { - manips = null; - return false; - } + if (Functions.FromCompressedBase64(manipString, out manips!) == MetaManipulation.CurrentVersion) + return true; - manips = []; - foreach (var manip in manipArray!.Where(m => m.Validate())) - { - if (manips.Add(manip)) - continue; - - Penumbra.Log.Warning($"Manipulation {manip} {manip.EntryToString()} is invalid and was skipped."); - manips = null; - return false; - } - - return true; + manips = null; + return false; } } diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index a8405eb2..15601867 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; @@ -49,7 +50,7 @@ public class TemporaryIpcTester( ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); - ImGui.InputTextWithHint("##tempManip", "Manipulation Base64 String...", ref _tempManipulation, 256); + ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 51149e3b..3ce54afb 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -52,16 +52,16 @@ public sealed class MetaDictionary : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool Add(IMetaIdentifier identifier, object entry) + public bool TryAdd(IMetaIdentifier identifier, object entry) => identifier switch { - EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && Add(eqdpIdentifier, e), - EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && Add(eqpIdentifier, e), - EstIdentifier estIdentifier => entry is EstEntry e && Add(estIdentifier, e), - GlobalEqpManipulation globalEqpManipulation => Add(globalEqpManipulation), - GmpIdentifier gmpIdentifier => entry is GmpEntry e && Add(gmpIdentifier, e), - ImcIdentifier imcIdentifier => entry is ImcEntry e && Add(imcIdentifier, e), - RspIdentifier rspIdentifier => entry is RspEntry e && Add(rspIdentifier, e), + EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && TryAdd(eqdpIdentifier, e), + EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && TryAdd(eqpIdentifier, e), + EstIdentifier estIdentifier => entry is EstEntry e && TryAdd(estIdentifier, e), + GlobalEqpManipulation globalEqpManipulation => TryAdd(globalEqpManipulation), + GmpIdentifier gmpIdentifier => entry is GmpEntry e && TryAdd(gmpIdentifier, e), + ImcIdentifier imcIdentifier => entry is ImcEntry e && TryAdd(imcIdentifier, e), + RspIdentifier rspIdentifier => entry is RspEntry e && TryAdd(rspIdentifier, e), _ => false, }; diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 021ee665..b0b588b4 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -76,7 +76,7 @@ public class ItemSwapContainer break; case IMetaSwap meta: if (!meta.SwapAppliedIsDefault) - convertedManips.Add(meta.SwapFromIdentifier, meta.SwapToModdedEntry); + convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry); break; } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 1813a7e3..396029d5 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -432,7 +432,7 @@ public class DebugTab : Window, ITab foreach (var obj in _objects) { - ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); + ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty From 196ca2ce393ba160c1bc7c29f9375feeb64f1b1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 8 Jun 2024 21:15:40 +0200 Subject: [PATCH 168/865] Remove all usages of Add(MetaManipulation) --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 137 +++++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 54 ++++--- Penumbra/Mods/SubMods/SubMod.cs | 6 +- Penumbra/Mods/TemporaryMod.cs | 4 +- 4 files changed, 121 insertions(+), 80 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 3ce54afb..1dc6496e 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -52,38 +52,6 @@ public sealed class MetaDictionary : IEnumerable IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool TryAdd(IMetaIdentifier identifier, object entry) - => identifier switch - { - EqdpIdentifier eqdpIdentifier => entry is EqdpEntryInternal e && TryAdd(eqdpIdentifier, e), - EqpIdentifier eqpIdentifier => entry is EqpEntryInternal e && TryAdd(eqpIdentifier, e), - EstIdentifier estIdentifier => entry is EstEntry e && TryAdd(estIdentifier, e), - GlobalEqpManipulation globalEqpManipulation => TryAdd(globalEqpManipulation), - GmpIdentifier gmpIdentifier => entry is GmpEntry e && TryAdd(gmpIdentifier, e), - ImcIdentifier imcIdentifier => entry is ImcEntry e && TryAdd(imcIdentifier, e), - RspIdentifier rspIdentifier => entry is RspEntry e && TryAdd(rspIdentifier, e), - _ => false, - }; - - public bool Add(MetaManipulation manip) - { - var ret = manip.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.TryAdd(manip.Imc.Identifier, manip.Imc.Entry), - MetaManipulation.Type.Eqdp => _eqdp.TryAdd(manip.Eqdp.Identifier, new EqdpEntryInternal(manip.Eqdp.Entry, manip.Eqdp.Slot)), - MetaManipulation.Type.Eqp => _eqp.TryAdd(manip.Eqp.Identifier, new EqpEntryInternal(manip.Eqp.Entry, manip.Eqp.Slot)), - MetaManipulation.Type.Est => _est.TryAdd(manip.Est.Identifier, manip.Est.Entry), - MetaManipulation.Type.Gmp => _gmp.TryAdd(manip.Gmp.Identifier, manip.Gmp.Entry), - MetaManipulation.Type.Rsp => _rsp.TryAdd(manip.Rsp.Identifier, manip.Rsp.Entry), - MetaManipulation.Type.GlobalEqp => _globalEqp.Add(manip.GlobalEqp), - _ => false, - }; - - if (ret) - ++Count; - return ret; - } - public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) { if (!_imc.TryAdd(identifier, entry)) @@ -93,9 +61,29 @@ public sealed class MetaDictionary : IEnumerable return true; } + + public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); + + public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(EqdpIdentifier identifier, EqdpEntry entry) => TryAdd(identifier, new EqdpEntryInternal(entry, identifier.Slot)); @@ -159,6 +147,73 @@ public sealed class MetaDictionary : IEnumerable TryAdd(identifier); } + /// Try to merge all manipulations from manips into this, and return the first failure, if any. + public bool MergeForced(MetaDictionary manips, out IMetaIdentifier failedIdentifier) + { + foreach (var (identifier, entry) in manips._imc) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._eqp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._eqdp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._gmp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._rsp) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var (identifier, entry) in manips._est) + { + if (!TryAdd(identifier, entry)) + { + failedIdentifier = identifier; + return false; + } + } + + foreach (var identifier in manips._globalEqp) + { + if (!TryAdd(identifier)) + { + failedIdentifier = identifier; + return false; + } + } + } + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) => _est.TryGetValue(identifier, out value); @@ -318,22 +373,4 @@ public sealed class MetaDictionary : IEnumerable return dict; } } - - private bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) - { - if (!_eqp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } - - private bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) - { - if (!_eqdp.TryAdd(identifier, entry)) - return false; - - ++Count; - return true; - } } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index b0b588b4..72a6005d 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -53,32 +53,38 @@ public class ItemSwapContainer { foreach (var swap in Swaps.SelectMany(s => s.WithChildren())) { - switch (swap) + if (swap is FileSwap file) { - case FileSwap file: - // Skip, nothing to do - if (file.SwapToModdedEqualsOriginal) - continue; + // Skip, nothing to do + if (file.SwapToModdedEqualsOriginal) + continue; - if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) - { - convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); - } - else - { - var path = file.GetNewPath(directory.FullName); - var bytes = file.FileData.Write(); - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - _manager.Compactor.WriteAllBytes(path, bytes); - convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); - } - - break; - case IMetaSwap meta: - if (!meta.SwapAppliedIsDefault) - convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry); - - break; + if (writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged) + { + convertedSwaps.TryAdd(file.SwapFromRequestPath, file.SwapToModded); + } + else + { + var path = file.GetNewPath(directory.FullName); + var bytes = file.FileData.Write(); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _manager.Compactor.WriteAllBytes(path, bytes); + convertedFiles.TryAdd(file.SwapFromRequestPath, new FullPath(path)); + } + } + else if (swap is IMetaSwap { SwapAppliedIsDefault: false }) + { + // @formatter:off + _ = swap switch + { + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwap meta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwapmeta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + MetaSwapmeta => convertedManips.TryAdd(meta.SwapFromIdentifier, meta.SwapToModdedEntry), + _ => false, + }; + // @formatter:on } } diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 06a924c8..40fd2e75 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -64,11 +64,9 @@ public static class SubMod data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } - var manips = json[nameof(data.Manipulations)]; + var manips = json[nameof(data.Manipulations)]?.ToObject(); if (manips != null) - foreach (var s in manips.Children().Select(c => c.ToObject()) - .Where(m => m.Validate())) - data.Manipulations.Add(s); + data.Manipulations.UnionWith(manips); } /// Load the relevant data for a selectable option from a JToken of that option. diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index a715f786..61ed4528 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -93,8 +93,8 @@ public class TemporaryMod : IMod } } - foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) - defaultMod.Manipulations.Add(manip); + MetaDictionary manips = [.. collection.MetaCache?.Manipulations ?? []]; + defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); From 361082813b59c125949c3a3c092945a2844626e3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Jun 2024 12:23:08 +0200 Subject: [PATCH 169/865] tmp --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 198 +++++-- .../Meta/Manipulations/MetaManipulation.cs | 142 ++++- Penumbra/Mods/Editor/ModMerger.cs | 9 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 167 +----- Penumbra/Mods/Groups/ImcModGroup.cs | 14 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 2 +- Penumbra/Mods/ModCreator.cs | 6 +- Penumbra/Mods/SubMods/SubMod.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 3 +- Penumbra/Services/StaticServiceManager.cs | 1 - .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 353 ++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 522 ++++++++++-------- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 19 +- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 11 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 207 +------ Penumbra/Util/DictionaryExtensions.cs | 12 + 16 files changed, 1008 insertions(+), 660 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 1dc6496e..236157ae 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -7,7 +7,7 @@ using ImcEntry = Penumbra.GameData.Structs.ImcEntry; namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] -public sealed class MetaDictionary : IEnumerable +public class MetaDictionary : IEnumerable { private readonly Dictionary _imc = []; private readonly Dictionary _eqp = []; @@ -17,8 +17,37 @@ public sealed class MetaDictionary : IEnumerable private readonly Dictionary _gmp = []; private readonly HashSet _globalEqp = []; + public IReadOnlyDictionary Imc + => _imc; + public int Count { get; private set; } + public int GetCount(MetaManipulation.Type type) + => type switch + { + MetaManipulation.Type.Imc => _imc.Count, + MetaManipulation.Type.Eqdp => _eqdp.Count, + MetaManipulation.Type.Eqp => _eqp.Count, + MetaManipulation.Type.Est => _est.Count, + MetaManipulation.Type.Gmp => _gmp.Count, + MetaManipulation.Type.Rsp => _rsp.Count, + MetaManipulation.Type.GlobalEqp => _globalEqp.Count, + _ => 0, + }; + + public bool CanAdd(IMetaIdentifier identifier) + => identifier switch + { + EqdpIdentifier eqdpIdentifier => !_eqdp.ContainsKey(eqdpIdentifier), + EqpIdentifier eqpIdentifier => !_eqp.ContainsKey(eqpIdentifier), + EstIdentifier estIdentifier => !_est.ContainsKey(estIdentifier), + GlobalEqpManipulation globalEqpManipulation => !_globalEqp.Contains(globalEqpManipulation), + GmpIdentifier gmpIdentifier => !_gmp.ContainsKey(gmpIdentifier), + ImcIdentifier imcIdentifier => !_imc.ContainsKey(imcIdentifier), + RspIdentifier rspIdentifier => !_rsp.ContainsKey(rspIdentifier), + _ => false, + }; + public void Clear() { _imc.Clear(); @@ -123,6 +152,68 @@ public sealed class MetaDictionary : IEnumerable return true; } + public bool Update(ImcIdentifier identifier, ImcEntry entry) + { + if (!_imc.ContainsKey(identifier)) + return false; + + _imc[identifier] = entry; + return true; + } + + + public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) + { + if (!_eqp.ContainsKey(identifier)) + return false; + + _eqp[identifier] = entry; + return true; + } + + public bool Update(EqpIdentifier identifier, EqpEntry entry) + => Update(identifier, new EqpEntryInternal(entry, identifier.Slot)); + + + public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + if (!_eqdp.ContainsKey(identifier)) + return false; + + _eqdp[identifier] = entry; + return true; + } + + public bool Update(EqdpIdentifier identifier, EqdpEntry entry) + => Update(identifier, new EqdpEntryInternal(entry, identifier.Slot)); + + public bool Update(EstIdentifier identifier, EstEntry entry) + { + if (!_est.ContainsKey(identifier)) + return false; + + _est[identifier] = entry; + return true; + } + + public bool Update(GmpIdentifier identifier, GmpEntry entry) + { + if (!_gmp.ContainsKey(identifier)) + return false; + + _gmp[identifier] = entry; + return true; + } + + public bool Update(RspIdentifier identifier, RspEntry entry) + { + if (!_rsp.ContainsKey(identifier)) + return false; + + _rsp[identifier] = entry; + return true; + } + public void UnionWith(MetaDictionary manips) { foreach (var (identifier, entry) in manips._imc) @@ -148,70 +239,52 @@ public sealed class MetaDictionary : IEnumerable } /// Try to merge all manipulations from manips into this, and return the first failure, if any. - public bool MergeForced(MetaDictionary manips, out IMetaIdentifier failedIdentifier) + public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier) { - foreach (var (identifier, entry) in manips._imc) + foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._eqp) + foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._eqdp) + foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._gmp) + foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._rsp) + foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var (identifier, entry) in manips._est) + foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { - if (!TryAdd(identifier, entry)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } - foreach (var identifier in manips._globalEqp) + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { - if (!TryAdd(identifier)) - { - failedIdentifier = identifier; - return false; - } + failedIdentifier = identifier; + return false; } + + failedIdentifier = default; + return false; } public bool TryGetValue(EstIdentifier identifier, out EstEntry value) @@ -244,6 +317,18 @@ public sealed class MetaDictionary : IEnumerable Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; } + public void UpdateTo(MetaDictionary other) + { + _imc.UpdateTo(other._imc); + _eqp.UpdateTo(other._eqp); + _eqdp.UpdateTo(other._eqdp); + _est.UpdateTo(other._est); + _rsp.UpdateTo(other._rsp); + _gmp.UpdateTo(other._gmp); + _globalEqp.UnionWith(other._globalEqp); + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + } + public MetaDictionary Clone() { var ret = new MetaDictionary(); @@ -251,6 +336,31 @@ public sealed class MetaDictionary : IEnumerable return ret; } + private static void WriteJson(JsonWriter writer, JsonSerializer serializer, IMetaIdentifier identifier, object entry) + { + var type = identifier switch + { + ImcIdentifier => "Imc", + EqdpIdentifier => "Eqdp", + EqpIdentifier => "Eqp", + EstIdentifier => "Est", + GmpIdentifier => "Gmp", + RspIdentifier => "Rsp", + GlobalEqpManipulation => "GlobalEqp", + _ => string.Empty, + }; + + if (type.Length == 0) + return; + + writer.WriteStartObject(); + writer.WritePropertyName("Type"); + writer.WriteValue(type); + writer.WritePropertyName("Manipulation"); + + writer.WriteEndObject(); + } + private class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, MetaDictionary? value, JsonSerializer serializer) diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index f22de809..b80681d2 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -1,9 +1,148 @@ +using Dalamud.Interface; +using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using OtterGui; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mods.Editor; using Penumbra.String.Functions; +using Penumbra.UI; +using Penumbra.UI.ModsTab; -namespace Penumbra.Meta.Manipulations; +namespace Penumbra.Meta.Manipulations; + +#if false +private static class ImcRow +{ + private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; + + private static float IdWidth + => 80 * UiHelpers.Scale; + + private static float SmallIdWidth + => 45 * UiHelpers.Scale; + + public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, + editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); + ImGui.TableNextColumn(); + var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); + var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); + var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); + var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) + editor.MetaEditor.Add(manip); + + // Identifier + ImGui.TableNextColumn(); + var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); + + ImGui.TableNextColumn(); + change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + + ImGui.TableNextColumn(); + // Equipment and accessories are slightly different imcs than other types. + if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); + else + change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); + + ImGui.TableNextColumn(); + change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); + + ImGui.TableNextColumn(); + if (_newIdentifier.ObjectType is ObjectType.DemiHuman) + change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); + else + ImUtf8.ScaledDummy(new Vector2(70 * UiHelpers.Scale, 0)); + + if (change) + defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; + // Values + using var disabled = ImRaii.Disabled(); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); + ImGui.SameLine(); + ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); + ImGui.TableNextColumn(); + ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); + } + + public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) + { + DrawMetaButtons(meta, editor, iconSize); + + // Identifier + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.ObjectType.ToName()); + ImGuiUtil.HoverTooltip(ObjectTypeTooltip); + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.PrimaryId.ToString()); + ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImGui.TextUnformatted(meta.EquipSlot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + else + { + ImGui.TextUnformatted(meta.SecondaryId.ToString()); + ImGuiUtil.HoverTooltip(SecondaryIdTooltip); + } + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + ImGui.TextUnformatted(meta.Variant.ToString()); + ImGuiUtil.HoverTooltip(VariantIdTooltip); + + ImGui.TableNextColumn(); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); + if (meta.ObjectType is ObjectType.DemiHuman) + { + ImGui.TextUnformatted(meta.EquipSlot.ToName()); + ImGuiUtil.HoverTooltip(EquipSlotTooltip); + } + + // Values + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); + ImGui.TableNextColumn(); + var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; + var newEntry = meta.Entry; + var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); + ImGui.SameLine(); + changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); + ImGui.TableNextColumn(); + changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); + ImGui.SameLine(); + changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); + ImGui.SameLine(); + changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); + ImGui.TableNextColumn(); + changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); + + if (changes) + editor.MetaEditor.Change(meta.Copy(newEntry)); + } +} + +#endif public interface IMetaManipulation { @@ -315,3 +454,4 @@ public readonly struct MetaManipulation : IEquatable, ICompara public static bool operator >=(MetaManipulation left, MetaManipulation right) => left.CompareTo(right) >= 0; } + diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 4faced80..2df76838 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -162,12 +162,9 @@ public class ModMerger : IDisposable foreach (var originalOption in mergeOptions) { - foreach (var manip in originalOption.Manipulations) - { - if (!manips.Add(manip)) - throw new Exception( - $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); - } + if (!manips.MergeForced(originalOption.Manipulations, out var failed)) + throw new Exception( + $"Could not add meta manipulation {failed} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); foreach (var (swapA, swapB) in originalOption.FileSwaps) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 45d9f8a1..42171378 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,24 +1,24 @@ using System.Collections.Frozen; +using OtterGui.Services; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager) +public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService { - private readonly HashSet _imc = []; - private readonly HashSet _eqp = []; - private readonly HashSet _eqdp = []; - private readonly HashSet _gmp = []; - private readonly HashSet _est = []; - private readonly HashSet _rsp = []; - private readonly HashSet _globalEqp = []; - public sealed class OtherOptionData : HashSet { public int TotalCount; + public void Add(string name, int count) + { + if (count > 0) + Add(name); + TotalCount += count; + } + public new void Clear() { TotalCount = 0; @@ -31,91 +31,9 @@ public class ModMetaEditor(ModManager modManager) public bool Changes { get; private set; } - public IReadOnlySet Imc - => _imc; - - public IReadOnlySet Eqp - => _eqp; - - public IReadOnlySet Eqdp - => _eqdp; - - public IReadOnlySet Gmp - => _gmp; - - public IReadOnlySet Est - => _est; - - public IReadOnlySet Rsp - => _rsp; - - public IReadOnlySet GlobalEqp - => _globalEqp; - - public bool CanAdd(MetaManipulation m) + public new void Clear() { - return m.ManipulationType switch - { - MetaManipulation.Type.Imc => !_imc.Contains(m.Imc), - MetaManipulation.Type.Eqdp => !_eqdp.Contains(m.Eqdp), - MetaManipulation.Type.Eqp => !_eqp.Contains(m.Eqp), - MetaManipulation.Type.Est => !_est.Contains(m.Est), - MetaManipulation.Type.Gmp => !_gmp.Contains(m.Gmp), - MetaManipulation.Type.Rsp => !_rsp.Contains(m.Rsp), - MetaManipulation.Type.GlobalEqp => !_globalEqp.Contains(m.GlobalEqp), - _ => false, - }; - } - - public bool Add(MetaManipulation m) - { - var added = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Add(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Add(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Add(m.Eqp), - MetaManipulation.Type.Est => _est.Add(m.Est), - MetaManipulation.Type.Gmp => _gmp.Add(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Add(m.Rsp), - MetaManipulation.Type.GlobalEqp => _globalEqp.Add(m.GlobalEqp), - _ => false, - }; - Changes |= added; - return added; - } - - public bool Delete(MetaManipulation m) - { - var deleted = m.ManipulationType switch - { - MetaManipulation.Type.Imc => _imc.Remove(m.Imc), - MetaManipulation.Type.Eqdp => _eqdp.Remove(m.Eqdp), - MetaManipulation.Type.Eqp => _eqp.Remove(m.Eqp), - MetaManipulation.Type.Est => _est.Remove(m.Est), - MetaManipulation.Type.Gmp => _gmp.Remove(m.Gmp), - MetaManipulation.Type.Rsp => _rsp.Remove(m.Rsp), - MetaManipulation.Type.GlobalEqp => _globalEqp.Remove(m.GlobalEqp), - _ => false, - }; - Changes |= deleted; - return deleted; - } - - public bool Change(MetaManipulation m) - => Delete(m) && Add(m); - - public bool Set(MetaManipulation m) - => Delete(m) | Add(m); - - public void Clear() - { - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _gmp.Clear(); - _est.Clear(); - _rsp.Clear(); - _globalEqp.Clear(); + base.Clear(); Changes = true; } @@ -129,15 +47,19 @@ public class ModMetaEditor(ModManager modManager) if (option == currentOption) continue; - foreach (var manip in option.Manipulations) - { - var data = OtherData[manip.ManipulationType]; - ++data.TotalCount; - data.Add(option.GetFullName()); - } + var name = option.GetFullName(); + OtherData[MetaManipulation.Type.Imc].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Imc)); + OtherData[MetaManipulation.Type.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqp)); + OtherData[MetaManipulation.Type.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqdp)); + OtherData[MetaManipulation.Type.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Gmp)); + OtherData[MetaManipulation.Type.Est].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Est)); + OtherData[MetaManipulation.Type.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Rsp)); + OtherData[MetaManipulation.Type.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.GlobalEqp)); } - Split(currentOption.Manipulations); + Clear(); + UnionWith(currentOption.Manipulations); + Changes = false; } public void Apply(IModDataContainer container) @@ -145,50 +67,7 @@ public class ModMetaEditor(ModManager modManager) if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, [..Recombine()]); + modManager.OptionEditor.SetManipulations(container, this); Changes = false; } - - private void Split(IEnumerable manips) - { - Clear(); - foreach (var manip in manips) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - _imc.Add(manip.Imc); - break; - case MetaManipulation.Type.Eqdp: - _eqdp.Add(manip.Eqdp); - break; - case MetaManipulation.Type.Eqp: - _eqp.Add(manip.Eqp); - break; - case MetaManipulation.Type.Est: - _est.Add(manip.Est); - break; - case MetaManipulation.Type.Gmp: - _gmp.Add(manip.Gmp); - break; - case MetaManipulation.Type.Rsp: - _rsp.Add(manip.Rsp); - break; - case MetaManipulation.Type.GlobalEqp: - _globalEqp.Add(manip.GlobalEqp); - break; - } - } - - Changes = false; - } - - public IEnumerable Recombine() - => _imc.Select(m => (MetaManipulation)m) - .Concat(_eqdp.Select(m => (MetaManipulation)m)) - .Concat(_eqp.Select(m => (MetaManipulation)m)) - .Concat(_est.Select(m => (MetaManipulation)m)) - .Concat(_gmp.Select(m => (MetaManipulation)m)) - .Concat(_rsp.Select(m => (MetaManipulation)m)) - .Concat(_globalEqp.Select(m => (MetaManipulation)m)); } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 383bc9fd..c52828c0 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -95,28 +95,28 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcManipulation GetManip(ushort mask, Variant variant) - => new(Identifier.ObjectType, Identifier.BodySlot, Identifier.PrimaryId, Identifier.SecondaryId.Id, variant.Id, - Identifier.EquipSlot, DefaultEntry with { AttributeMask = mask }); + public ImcEntry GetEntry(ushort mask) + => DefaultEntry with { AttributeMask = mask }; public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; - var mask = GetCurrentMask(setting); + var mask = GetCurrentMask(setting); + var entry = GetEntry(mask); if (AllVariants) { var count = ImcChecker.GetVariantCount(Identifier); if (count == 0) - manipulations.Add(GetManip(mask, Identifier.Variant)); + manipulations.TryAdd(Identifier, entry); else for (var i = 0; i <= count; ++i) - manipulations.Add(GetManip(mask, (Variant)i)); + manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry); } else { - manipulations.Add(GetManip(mask, Identifier.Variant)); + manipulations.TryAdd(Identifier, entry); } } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 72a6005d..8328edea 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -124,7 +124,7 @@ public class ItemSwapContainer private MetaDictionary MetaResolver(ModCollection? collection) => collection?.MetaCache?.Manipulations is { } cache - ? [.. cache] + ? [] // [.. cache] TODO : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0035fd41..e8ca3199 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -198,7 +198,8 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith([.. meta.MetaManipulations]); + // TODO + option.Manipulations.UnionWith([]);//[.. meta.MetaManipulations]); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { @@ -212,7 +213,8 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.Manipulations.UnionWith([.. rgsp.MetaManipulations]); + // TODO + option.Manipulations.UnionWith([]);//[.. rgsp.MetaManipulations]); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 40fd2e75..a8c37369 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -37,7 +37,7 @@ public static class SubMod { to.Files = new Dictionary(from.Files); to.FileSwaps = new Dictionary(from.FileSwaps); - to.Manipulations = [.. from.Manipulations]; + to.Manipulations = from.Manipulations.Clone(); } /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 61ed4528..91c4c5df 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -93,7 +93,8 @@ public class TemporaryMod : IMod } } - MetaDictionary manips = [.. collection.MetaCache?.Manipulations ?? []]; + // TODO + MetaDictionary manips = []; // [.. collection.MetaCache?.Manipulations ?? []]; defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 0c6648ba..35e36349 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -188,7 +188,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs new file mode 100644 index 00000000..d9a8c27c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -0,0 +1,353 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + private bool _fileExists; + + private const string ModelSetIdTooltipShort = "Model Set ID"; + private const string EquipSlotTooltip = "Equip Slot"; + private const string ModelRaceTooltip = "Model Race"; + private const string GenderTooltip = "Gender"; + private const string ObjectTypeTooltip = "Object Type"; + private const string SecondaryIdTooltip = "Secondary ID"; + private const string PrimaryIdTooltipShort = "Primary ID"; + private const string VariantIdTooltip = "Variant ID"; + private const string EstTypeTooltip = "EST Type"; + private const string RacialTribeTooltip = "Racial Tribe"; + private const string ScalingTypeTooltip = "Scaling Type"; + + protected override void Initialize() + { + Identifier = ImcIdentifier.Default; + UpdateEntry(); + } + + private void UpdateEntry() + => (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + // Copy To Clipboard + ImGui.TableNextColumn(); + var canAdd = _fileExists && Editor.MetaEditor.CanAdd(Identifier); + var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.MetaEditor.TryAdd(Identifier, Entry); + + if (DrawIdentifier(ref Identifier)) + UpdateEntry(); + + using var disabled = ImRaii.Disabled(); + DrawEntry(Entry, ref Entry, false); + } + + protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry) + { + const uint frameColor = 0; + // Meta Buttons + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.ObjectType.ToName(), frameColor); + ImUtf8.HoverTooltip("Object Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", frameColor); + ImUtf8.HoverTooltip("Primary ID"); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + else + { + ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", frameColor); + ImUtf8.HoverTooltip("Secondary ID"u8); + } + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.Variant.Id}", frameColor); + ImUtf8.HoverTooltip("Variant"u8); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; + if (DrawEntry(defaultEntry, ref entry, true)) + Editor.MetaEditor.Update(identifier, entry); + } + + private static bool DrawIdentifier(ref ImcIdentifier identifier) + { + ImGui.TableNextColumn(); + var change = DrawObjectType(ref identifier); + + ImGui.TableNextColumn(); + change |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + change |= DrawSlot(ref identifier); + else + change |= DrawSecondaryId(ref identifier); + + ImGui.TableNextColumn(); + change |= DrawVariant(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + change |= DrawSlot(ref identifier, 70f); + else + ImUtf8.ScaledDummy(70f); + return change; + } + + private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) + { + ImGui.TableNextColumn(); + var change = DrawMaterialId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawMaterialAnimationId(defaultEntry, ref entry, addDefault); + + ImGui.TableNextColumn(); + change |= DrawDecalId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawVfxId(defaultEntry, ref entry, addDefault); + ImUtf8.SameLineInner(); + change |= DrawSoundId(defaultEntry, ref entry, addDefault); + + ImGui.TableNextColumn(); + change |= DrawAttributes(defaultEntry, ref entry); + return change; + } + + + protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() + => Editor.MetaEditor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) + { + var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); + ImUtf8.HoverTooltip("Object Type"u8); + + if (ret) + { + var equipSlot = type switch + { + ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, + _ => EquipSlot.Unknown, + }; + identifier = identifier with + { + ObjectType = type, + EquipSlot = equipSlot, + SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + }; + } + + return ret; + } + + public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) + { + var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, + identifier.PrimaryId.Id <= 1); + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 + + "This should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { PrimaryId = newId }; + return ret; + } + + public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); + ImUtf8.HoverTooltip("Secondary ID"u8); + if (ret) + identifier = identifier with { SecondaryId = newId }; + return ret; + } + + public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) + { + var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); + ImUtf8.HoverTooltip("Variant ID"u8); + if (ret) + identifier = identifier with { Variant = (byte)newId }; + return ret; + } + + public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) + { + bool ret; + EquipSlot slot; + switch (identifier.ObjectType) + { + case ObjectType.Equipment: + case ObjectType.DemiHuman: + ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); + break; + case ObjectType.Accessory: + ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); + break; + default: return false; + } + + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { EquipSlot = slot }; + return ret; + } + + public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, + out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialId = newValue }; + return true; + } + + public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, + defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { MaterialAnimationId = newValue }; + return true; + } + + public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { DecalId = newValue }; + return true; + } + + public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, + byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { VfxId = newValue }; + return true; + } + + public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) + { + if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, + (byte)0, byte.MaxValue, 0.01f, addDefault)) + return false; + + entry = entry with { SoundId = newValue }; + return true; + } + + public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + { + var changes = false; + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (entry.AttributeMask & flag) != 0; + var def = (defaultEntry.AttributeMask & flag) != 0; + if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) + { + var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); + entry = entry with { AttributeMask = newMask }; + changes = true; + } + + if (i < ImcEntry.NumAttributes - 1) + ImUtf8.SameLineInner(); + } + + return changes; + } + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index b0a74637..50862eec 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,10 +1,11 @@ +using System.Reflection.Emit; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; +using OtterGui.Text.EndObjects; using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; @@ -20,17 +21,7 @@ public partial class ModEditWindow private const string ModelSetIdTooltip = "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIdTooltipShort = "Primary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + private void DrawMetaTab() { @@ -56,7 +47,7 @@ public partial class ModEditWindow ImGui.SameLine(); SetFromClipboardButton(); ImGui.SameLine(); - CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); + CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) _metaFileManager.WriteAllTexToolsMeta(Mod!); @@ -65,71 +56,103 @@ public partial class ModEditWindow if (!child) return; - DrawEditHeader(_editor.MetaEditor.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqp]); - DrawEditHeader(_editor.MetaEditor.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Eqdp]); - DrawEditHeader(_editor.MetaEditor.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Imc]); - DrawEditHeader(_editor.MetaEditor.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Est]); - DrawEditHeader(_editor.MetaEditor.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Gmp]); - DrawEditHeader(_editor.MetaEditor.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew, - _editor.MetaEditor.OtherData[MetaManipulation.Type.Rsp]); - DrawEditHeader(_editor.MetaEditor.GlobalEqp, "Global Equipment Parameter Edits (Global EQP)###GEQP", 4, GlobalEqpRow.Draw, - GlobalEqpRow.DrawNew, _editor.MetaEditor.OtherData[MetaManipulation.Type.GlobalEqp]); + DrawEditHeader(MetaManipulation.Type.Eqp); + DrawEditHeader(MetaManipulation.Type.Eqdp); + DrawEditHeader(MetaManipulation.Type.Imc); + DrawEditHeader(MetaManipulation.Type.Est); + DrawEditHeader(MetaManipulation.Type.Gmp); + DrawEditHeader(MetaManipulation.Type.Rsp); + DrawEditHeader(MetaManipulation.Type.GlobalEqp); } - - /// The headers for the different meta changes all have basically the same structure for different types. - private void DrawEditHeader(IReadOnlyCollection items, string label, int numColumns, - Action draw, Action drawNew, - ModMetaEditor.OtherOptionData otherOptionData) - { - const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; - - var oldPos = ImGui.GetCursorPosY(); - var header = ImGui.CollapsingHeader($"{items.Count} {label}"); - var newPos = ImGui.GetCursorPos(); - if (otherOptionData.TotalCount > 0) + private static ReadOnlySpan Label(MetaManipulation.Type type) + => type switch { - var text = $"{otherOptionData.TotalCount} Edits in other Options"; - var size = ImGui.CalcTextSize(text).X; - ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); - ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); - if (ImGui.IsItemHovered()) - { - using var tt = ImUtf8.Tooltip(); - foreach (var name in otherOptionData) - ImUtf8.Text(name); - } + MetaManipulation.Type.Imc => "Variant Edits (IMC)###IMC"u8, + MetaManipulation.Type.Eqdp => "Racial Model Edits (EQDP)###EQDP"u8, + MetaManipulation.Type.Eqp => "Equipment Parameter Edits (EQP)###EQP"u8, + MetaManipulation.Type.Est => "Extra Skeleton Parameters (EST)###EST"u8, + MetaManipulation.Type.Gmp => "Visor/Gimmick Edits (GMP)###GMP"u8, + MetaManipulation.Type.Rsp => "Racial Scaling Edits (RSP)###RSP"u8, + MetaManipulation.Type.GlobalEqp => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8, + _ => "\0"u8, + }; - ImGui.SetCursorPos(newPos); - } + private static int ColumnCount(MetaManipulation.Type type) + => type switch + { + MetaManipulation.Type.Imc => 10, + MetaManipulation.Type.Eqdp => 7, + MetaManipulation.Type.Eqp => 5, + MetaManipulation.Type.Est => 7, + MetaManipulation.Type.Gmp => 7, + MetaManipulation.Type.Rsp => 5, + MetaManipulation.Type.GlobalEqp => 4, + _ => 0, + }; + private void DrawEditHeader(MetaManipulation.Type type) + { + var oldPos = ImGui.GetCursorPosY(); + var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {Label(type)}"); + DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos()); if (!header) return; - using (var table = ImRaii.Table(label, numColumns, flags)) - { - if (table) - { - drawNew(_metaFileManager, _editor, _iconSize); - foreach (var (item, index) in items.ToArray().WithIndex()) - { - using var id = ImRaii.PushId(index); - draw(_metaFileManager, item, _editor, _iconSize); - } - } - } + DrawTable(type); + } + private IMetaDrawer? Drawer(MetaManipulation.Type type) + => type switch + { + //MetaManipulation.Type.Imc => expr, + //MetaManipulation.Type.Eqdp => expr, + //MetaManipulation.Type.Eqp => expr, + //MetaManipulation.Type.Est => expr, + //MetaManipulation.Type.Gmp => expr, + //MetaManipulation.Type.Rsp => expr, + //MetaManipulation.Type.GlobalEqp => expr, + _ => null, + }; + + private void DrawTable(MetaManipulation.Type type) + { + const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; + using var table = ImUtf8.Table(Label(type), ColumnCount(type), flags); + if (!table) + return; + + if (Drawer(type) is not { } drawer) + return; + + drawer.Draw(); ImGui.NewLine(); } + private void DrawOtherOptionData(MetaManipulation.Type type, float oldPos, Vector2 newPos) + { + var otherOptionData = _editor.MetaEditor.OtherData[type]; + if (otherOptionData.TotalCount <= 0) + return; + + var text = $"{otherOptionData.TotalCount} Edits in other Options"; + var size = ImGui.CalcTextSize(text).X; + ImGui.SetCursorPos(new Vector2(ImGui.GetContentRegionAvail().X - size, oldPos + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(ColorId.RedundantAssignment.Value() | 0xFF000000, text); + if (ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + foreach (var name in otherOptionData) + ImUtf8.Text(name); + } + + ImGui.SetCursorPos(newPos); + } + +#if false private static class EqpRow { - private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); + private static EqpIdentifier _newIdentifier = new(1, EquipSlot.Body); private static float IdWidth => 100 * UiHelpers.Scale; @@ -140,8 +163,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -197,7 +220,7 @@ public partial class ModEditWindow var idx = 0; foreach (var flag in Eqp.EqpAttributes[meta.Slot]) { - using var id = ImRaii.PushId(idx++); + using var id = ImRaii.PushId(idx++); var defaultValue = defaultEntry.HasFlag(flag); var currentValue = meta.Entry.HasFlag(flag); if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value)) @@ -209,8 +232,6 @@ public partial class ModEditWindow ImGui.NewLine(); } } - - private static class EqdpRow { private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); @@ -224,9 +245,9 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var raceCode = Names.CombinedRace(_new.Gender, _new.Race); + var raceCode = Names.CombinedRace(_new.Gender, _new.Race); var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0; - var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); + var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); var tt = canAdd ? "Stage this edit." : validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; var defaultEntry = validRaceCode @@ -311,7 +332,7 @@ public partial class ModEditWindow var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); - var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); + var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); ImGui.TableNextColumn(); if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1)) editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2))); @@ -321,136 +342,7 @@ public partial class ModEditWindow editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2))); } } - - private static class ImcRow - { - private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; - - private static float IdWidth - => 80 * UiHelpers.Scale; - - private static float SmallIdWidth - => 45 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, - editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); - var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); - var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); - var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(manip); - - // Identifier - ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - - ImGui.TableNextColumn(); - // Equipment and accessories are slightly different imcs than other types. - if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); - else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); - - ImGui.TableNextColumn(); - if (_newIdentifier.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); - else - ImGui.Dummy(new Vector2(70 * UiHelpers.Scale, 0)); - - if (change) - defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); - } - - public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.ObjectType.ToName()); - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else - { - ImGui.TextUnformatted(meta.SecondaryId.ToString()); - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Variant.ToString()); - ImGuiUtil.HoverTooltip(VariantIdTooltip); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.DemiHuman) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - - // Values - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.TableNextColumn(); - var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; - var newEntry = meta.Entry; - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - - if (changes) - editor.MetaEditor.Change(meta.Copy(newEntry)); - } - } - + private static class EstRow { private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); @@ -464,8 +356,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, editor.MetaEditor.Est.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -538,12 +430,11 @@ public partial class ModEditWindow // Values var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, + if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, out var entry, 0, ushort.MaxValue, 0.05f)) editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); } } - private static class GmpRow { private static GmpManipulation _new = new(GmpEntry.Default, 1); @@ -563,8 +454,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -643,7 +534,6 @@ public partial class ModEditWindow editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); } } - private static class RspRow { private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); @@ -657,8 +547,8 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; + var canAdd = editor.MetaEditor.CanAdd(_new); + var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new.Copy(defaultEntry)); @@ -700,7 +590,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; + var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; var value = meta.Entry.Value; ImGui.SetNextItemWidth(FloatWidth); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, @@ -713,12 +603,11 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); } } - private static class GlobalEqpRow { private static GlobalEqpManipulation _new = new() { - Type = GlobalEqpType.DoNotHideEarrings, + Type = GlobalEqpType.DoNotHideEarrings, Condition = 1, }; @@ -729,7 +618,7 @@ public partial class ModEditWindow editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); ImGui.TableNextColumn(); var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; + var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) editor.MetaEditor.Add(_new); @@ -744,7 +633,7 @@ public partial class ModEditWindow if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) _new = new GlobalEqpManipulation { - Type = type, + Type = type, Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, }; ImUtf8.HoverTooltip(type.ToDescription()); @@ -777,6 +666,7 @@ public partial class ModEditWindow } } } +#endif // A number input for ids with a optional max id of given width. // Returns true if newId changed against currentId. @@ -824,7 +714,7 @@ public partial class ModEditWindow return newValue != currentValue; } - private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, IEnumerable manipulations) + private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; @@ -840,10 +730,9 @@ public partial class ModEditWindow { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); + var version = Functions.FromCompressedBase64(clipboard, out var manips); if (version == MetaManipulation.CurrentVersion && manips != null) - foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) - _editor.MetaEditor.Set(manip); + _editor.MetaEditor.UpdateTo(manips); } ImGuiUtil.HoverTooltip( @@ -855,13 +744,9 @@ public partial class ModEditWindow if (ImGui.Button("Set from Clipboard")) { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); + var version = Functions.FromCompressedBase64(clipboard, out var manips); if (version == MetaManipulation.CurrentVersion && manips != null) - { - _editor.MetaEditor.Clear(); - foreach (var manip in manips.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown)) - _editor.MetaEditor.Set(manip); - } + _editor.MetaEditor.SetTo(manips); } ImGuiUtil.HoverTooltip( @@ -870,11 +755,184 @@ public partial class ModEditWindow private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize) { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) - editor.MetaEditor.Delete(meta); + //ImGui.TableNextColumn(); + //CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); + // + //ImGui.TableNextColumn(); + //if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) + // editor.MetaEditor.Delete(meta); } +} + + +public interface IMetaDrawer +{ + public void Draw(); } + + + + +public abstract class MetaDrawer(ModEditor editor, MetaFileManager metaFiles) : IMetaDrawer + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + protected readonly ModEditor Editor = editor; + protected readonly MetaFileManager MetaFiles = metaFiles; + protected TIdentifier Identifier; + protected TEntry Entry; + private bool _initialized; + + public void Draw() + { + if (!_initialized) + { + Initialize(); + _initialized = true; + } + + DrawNew(); + foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) + { + using var id = ImUtf8.PushId(idx); + DrawEntry(identifier, entry); + } + } + + protected abstract void DrawNew(); + protected abstract void Initialize(); + protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); + + protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); +} + + +#if false +public sealed class GmpMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new GmpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) + { } + + protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class EstMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) + { } + + protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class EqdpMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqdpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntry entry) + { } + + protected override IEnumerable<(EqdpIdentifier, EqdpEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class EqpMetaDrawer(ModEditor editor, MetaFileManager metaManager) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(EqpIdentifier identifier, EqpEntry entry) + { } + + protected override IEnumerable<(EqpIdentifier, EqpEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + +public sealed class RspMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new RspIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) + { } + + protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} + + + +public sealed class GlobalEqpMetaDrawer(ModEditor editor) : MetaDrawer, IService +{ + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); + + protected override void DrawNew() + { } + + protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) + { } + + protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() + => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); +} +#endif diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 3ac10cd0..689571f3 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -9,6 +9,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab.Groups; @@ -79,29 +80,29 @@ public class AddGroupDrawer : IUiService private void DrawImcInput(float width) { - var change = ImcManipulationDrawer.DrawObjectType(ref _imcIdentifier, width); + var change = ImcMetaDrawer.DrawObjectType(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawPrimaryId(ref _imcIdentifier, width); if (_imcIdentifier.ObjectType is ObjectType.Weapon or ObjectType.Monster) { - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, width); } else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= ImcManipulationDrawer.DrawSecondaryId(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); + change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, quarterWidth); } else { - change |= ImcManipulationDrawer.DrawSlot(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, width); ImUtf8.SameLineInner(); - change |= ImcManipulationDrawer.DrawVariant(ref _imcIdentifier, width); + change |= ImcMetaDrawer.DrawVariant(ref _imcIdentifier, width); } if (change) diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 5c8edce6..5d10febd 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -7,6 +7,7 @@ using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; +using Penumbra.UI.AdvancedWindow.Meta; namespace Penumbra.UI.ModsTab.Groups; @@ -37,9 +38,9 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImGui.SameLine(); using (ImUtf8.Group()) { - changes |= ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true); } ImGui.SameLine(0, editor.PriorityWidth); @@ -54,8 +55,8 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr using (ImUtf8.Group()) { - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true); var canBeDisabled = group.CanBeDisabled; if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs index 694ae11c..1291f568 100644 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs @@ -10,210 +10,5 @@ namespace Penumbra.UI.ModsTab; public static class ImcManipulationDrawer { - public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) - { - var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); - ImUtf8.HoverTooltip("Object Type"u8); - - if (ret) - { - var equipSlot = type switch - { - ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, - }; - identifier = identifier with - { - ObjectType = type, - EquipSlot = equipSlot, - SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, - }; - } - - return ret; - } - - public static bool DrawPrimaryId(ref ImcIdentifier identifier, float unscaledWidth = 80) - { - var ret = IdInput("##imcPrimaryId"u8, unscaledWidth, identifier.PrimaryId.Id, out var newId, 0, ushort.MaxValue, - identifier.PrimaryId.Id <= 1); - ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'x####' part of an item path.\n"u8 - + "This should generally not be left <= 1 unless you explicitly want that."u8); - if (ret) - identifier = identifier with { PrimaryId = newId }; - return ret; - } - - public static bool DrawSecondaryId(ref ImcIdentifier identifier, float unscaledWidth = 100) - { - var ret = IdInput("##imcSecondaryId"u8, unscaledWidth, identifier.SecondaryId.Id, out var newId, 0, ushort.MaxValue, false); - ImUtf8.HoverTooltip("Secondary ID"u8); - if (ret) - identifier = identifier with { SecondaryId = newId }; - return ret; - } - - public static bool DrawVariant(ref ImcIdentifier identifier, float unscaledWidth = 45) - { - var ret = IdInput("##imcVariant"u8, unscaledWidth, identifier.Variant.Id, out var newId, 0, byte.MaxValue, false); - ImUtf8.HoverTooltip("Variant ID"u8); - if (ret) - identifier = identifier with { Variant = (byte)newId }; - return ret; - } - - public static bool DrawSlot(ref ImcIdentifier identifier, float unscaledWidth = 100) - { - bool ret; - EquipSlot slot; - switch (identifier.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.DemiHuman: - ret = Combos.EqpEquipSlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); - break; - case ObjectType.Accessory: - ret = Combos.AccessorySlot("##slot", identifier.EquipSlot, out slot, unscaledWidth); - break; - default: return false; - } - - ImUtf8.HoverTooltip("Equip Slot"u8); - if (ret) - identifier = identifier with { EquipSlot = slot }; - return ret; - } - - public static bool DrawMaterialId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##materialId"u8, "Material ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialId, defaultEntry.MaterialId, - out var newValue, (byte)1, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { MaterialId = newValue }; - return true; - } - - public static bool DrawMaterialAnimationId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##mAnimId"u8, "Material Animation ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.MaterialAnimationId, - defaultEntry.MaterialAnimationId, out var newValue, (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { MaterialAnimationId = newValue }; - return true; - } - - public static bool DrawDecalId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##decalId"u8, "Decal ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.DecalId, defaultEntry.DecalId, out var newValue, - (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { DecalId = newValue }; - return true; - } - - public static bool DrawVfxId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##vfxId"u8, "VFX ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.VfxId, defaultEntry.VfxId, out var newValue, (byte)0, - byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { VfxId = newValue }; - return true; - } - - public static bool DrawSoundId(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault, float unscaledWidth = 45) - { - if (!DragInput("##soundId"u8, "Sound ID"u8, unscaledWidth * ImUtf8.GlobalScale, entry.SoundId, defaultEntry.SoundId, out var newValue, - (byte)0, byte.MaxValue, 0.01f, addDefault)) - return false; - - entry = entry with { SoundId = newValue }; - return true; - } - - public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) - { - var changes = false; - for (var i = 0; i < ImcEntry.NumAttributes; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - var value = (entry.AttributeMask & flag) != 0; - var def = (defaultEntry.AttributeMask & flag) != 0; - if (Checkmark("##attribute"u8, "ABCDEFGHIJ"u8.Slice(i, 1), value, def, out var newValue)) - { - var newMask = (ushort)(newValue ? entry.AttributeMask | flag : entry.AttributeMask & ~flag); - entry = entry with { AttributeMask = newMask }; - changes = true; - } - - if (i < ImcEntry.NumAttributes - 1) - ImGui.SameLine(); - } - - return changes; - } - - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - /// - /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - /// Returns true if newValue changed against currentValue. - /// - private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, - out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) - newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; - - if (addDefault) - ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - - return newValue != currentValue; - } - - /// - /// A checkmark that compares against a default value and shows a tooltip. - /// Returns true if newValue is changed against currentValue. - /// - private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, - out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImUtf8.Checkbox(label, ref newValue); - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - return newValue != currentValue; - } + } diff --git a/Penumbra/Util/DictionaryExtensions.cs b/Penumbra/Util/DictionaryExtensions.cs index abf715e6..f7aa5598 100644 --- a/Penumbra/Util/DictionaryExtensions.cs +++ b/Penumbra/Util/DictionaryExtensions.cs @@ -45,6 +45,18 @@ public static class DictionaryExtensions lhs.Add(key, value); } + /// Set all entries in the right-hand dictionary to the same values in the left-hand dictionary, ensuring capacity beforehand. + public static void UpdateTo(this Dictionary lhs, IReadOnlyDictionary rhs) + where TKey : notnull + { + if (ReferenceEquals(lhs, rhs)) + return; + + lhs.EnsureCapacity(rhs.Count); + foreach (var (key, value) in rhs) + lhs[key] = value; + } + /// Set one set to the other, deleting previous entries and ensuring capacity beforehand. public static void SetTo(this HashSet lhs, IReadOnlySet rhs) { From 3170edfeb659f0362d46f56fde9c000ee54ba0ee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 13:38:36 +0200 Subject: [PATCH 170/865] Get rid off all MetaManipulation things. --- Penumbra/Api/Api/MetaApi.cs | 28 +- Penumbra/Api/Api/TemporaryApi.cs | 4 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 8 +- Penumbra/Collections/Cache/CmpCache.cs | 56 -- Penumbra/Collections/Cache/CollectionCache.cs | 40 +- .../Collections/Cache/CollectionModData.cs | 12 +- Penumbra/Collections/Cache/EqdpCache.cs | 85 +- Penumbra/Collections/Cache/EqpCache.cs | 77 +- Penumbra/Collections/Cache/EstCache.cs | 136 ++- .../Cache}/GlobalEqpCache.cs | 44 +- Penumbra/Collections/Cache/GmpCache.cs | 74 +- Penumbra/Collections/Cache/IMetaCache.cs | 89 ++ Penumbra/Collections/Cache/ImcCache.cs | 153 ++-- Penumbra/Collections/Cache/MetaCache.cs | 269 +++--- Penumbra/Collections/Cache/RspCache.cs | 81 ++ Penumbra/Import/Models/ModelManager.cs | 23 +- .../Import/TexToolsMeta.Deserialization.cs | 61 +- Penumbra/Import/TexToolsMeta.Export.cs | 328 ++++--- Penumbra/Import/TexToolsMeta.Rgsp.cs | 17 +- Penumbra/Import/TexToolsMeta.cs | 25 +- .../PathResolving/CollectionResolver.cs | 1 - .../ResolveContext.PathResolution.cs | 2 +- Penumbra/Meta/ImcChecker.cs | 4 - Penumbra/Meta/Manipulations/Eqdp.cs | 9 + .../Meta/Manipulations/EqdpManipulation.cs | 110 --- Penumbra/Meta/Manipulations/Eqp.cs | 3 + .../Meta/Manipulations/EqpManipulation.cs | 80 -- Penumbra/Meta/Manipulations/Est.cs | 16 + .../Meta/Manipulations/EstManipulation.cs | 108 --- .../Manipulations/GlobalEqpManipulation.cs | 26 +- Penumbra/Meta/Manipulations/Gmp.cs | 3 + .../Meta/Manipulations/GmpManipulation.cs | 58 -- .../Meta/Manipulations/IMetaIdentifier.cs | 16 + Penumbra/Meta/Manipulations/Imc.cs | 6 +- .../Meta/Manipulations/ImcManipulation.cs | 108 --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 338 +++++-- .../Meta/Manipulations/MetaManipulation.cs | 457 ---------- Penumbra/Meta/Manipulations/Rsp.cs | 3 + .../Meta/Manipulations/RspManipulation.cs | 67 -- Penumbra/Mods/Editor/IMod.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 24 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 41 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 29 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 +- Penumbra/Mods/ModCreator.cs | 6 +- Penumbra/Mods/SubMods/DefaultSubMod.cs | 2 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 3 +- .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 159 ++++ .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 134 +++ .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 147 +++ .../Meta/GlobalEqpMetaDrawer.cs | 111 +++ .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 148 +++ .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 164 ++-- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 154 ++++ .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 35 + .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 112 +++ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 847 +----------------- .../ModEditWindow.Models.MdlTab.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 6 +- Penumbra/UI/Tabs/EffectiveTab.cs | 10 +- Penumbra/Util/IdentifierExtensions.cs | 94 +- 63 files changed, 2422 insertions(+), 2847 deletions(-) delete mode 100644 Penumbra/Collections/Cache/CmpCache.cs rename Penumbra/{Meta/Manipulations => Collections/Cache}/GlobalEqpCache.cs (75%) create mode 100644 Penumbra/Collections/Cache/IMetaCache.cs create mode 100644 Penumbra/Collections/Cache/RspCache.cs delete mode 100644 Penumbra/Meta/Manipulations/EqdpManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/EqpManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/EstManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/GmpManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/ImcManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/MetaManipulation.cs delete mode 100644 Penumbra/Meta/Manipulations/RspManipulation.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index c467df58..ce1a9def 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -1,5 +1,8 @@ +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; @@ -7,17 +10,34 @@ namespace Penumbra.Api.Api; public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService { + public const int CurrentVersion = 0; + public string GetPlayerMetaManipulations() { var collection = collectionResolver.PlayerCollection(); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + return CompressMetaManipulations(collection); } public string GetMetaManipulations(int gameObjectIdx) { helpers.AssociatedCollection(gameObjectIdx, out var collection); - var set = collection.MetaCache?.Manipulations.ToArray() ?? []; - return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); + return CompressMetaManipulations(collection); + } + + internal static string CompressMetaManipulations(ModCollection collection) + { + var array = new JArray(); + if (collection.MetaCache is { } cache) + { + MetaDictionary.SerializeTo(array, cache.GlobalEqp.Select(kvp => kvp.Key)); + MetaDictionary.SerializeTo(array, cache.Imc.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Eqp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Eqdp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + } + + return Functions.ToCompressedBase64(array, CurrentVersion); } } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 49958a0d..0894a8e5 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -163,11 +163,11 @@ public class TemporaryApi( { if (manipString.Length == 0) { - manips = []; + manips = new MetaDictionary(); return true; } - if (Functions.FromCompressedBase64(manipString, out manips!) == MetaManipulation.CurrentVersion) + if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion) return true; manips = null; diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 15601867..0aa6821c 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -5,6 +5,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; @@ -102,8 +103,7 @@ public class TemporaryIpcTester( && copyCollection is { HasCache: true }) { var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); - var manips = Functions.ToCompressedBase64(copyCollection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(), - MetaManipulation.CurrentVersion); + var manips = MetaApi.CompressMetaManipulations(copyCollection); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); } @@ -188,8 +188,8 @@ public class TemporaryIpcTester( if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - foreach (var manip in mod.Default.Manipulations) - ImGui.TextUnformatted(manip.ToString()); + foreach (var identifier in mod.Default.Manipulations.Identifiers) + ImGui.TextUnformatted(identifier.ToString()); } } } diff --git a/Penumbra/Collections/Cache/CmpCache.cs b/Penumbra/Collections/Cache/CmpCache.cs deleted file mode 100644 index 470cadd4..00000000 --- a/Penumbra/Collections/Cache/CmpCache.cs +++ /dev/null @@ -1,56 +0,0 @@ -using OtterGui.Filesystem; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Collections.Cache; - -public struct CmpCache : IDisposable -{ - private CmpFile? _cmpFile = null; - private readonly List _cmpManipulations = new(); - - public CmpCache() - { } - - public void SetFiles(MetaFileManager manager) - => manager.SetFile(_cmpFile, MetaIndex.HumanCmp); - - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); - - public void Reset() - { - if (_cmpFile == null) - return; - - _cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute))); - _cmpManipulations.Clear(); - } - - public bool ApplyMod(MetaFileManager manager, RspManipulation manip) - { - _cmpManipulations.AddOrReplace(manip); - _cmpFile ??= new CmpFile(manager); - return manip.Apply(_cmpFile); - } - - public bool RevertMod(MetaFileManager manager, RspManipulation manip) - { - if (!_cmpManipulations.Remove(manip)) - return false; - - var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute); - manip = new RspManipulation(manip.SubRace, manip.Attribute, def); - return manip.Apply(_cmpFile!); - } - - public void Dispose() - { - _cmpFile?.Dispose(); - _cmpFile = null; - _cmpManipulations.Clear(); - } -} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 4d8d0b4a..fd801d3b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -36,7 +36,7 @@ public sealed class CollectionCache : IDisposable => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -233,8 +233,20 @@ public sealed class CollectionCache : IDisposable foreach (var (path, file) in files.FileRedirections) AddFile(path, file, mod); - foreach (var manip in files.Manipulations) - AddManipulation(manip, mod); + foreach (var (identifier, entry) in files.Manipulations.Eqp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Eqdp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Est) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Gmp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Rsp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Imc) + AddManipulation(mod, identifier, entry); + foreach (var identifier in files.Manipulations.GlobalEqp) + AddManipulation(mod, identifier, null!); if (addMetaChanges) { @@ -342,7 +354,7 @@ public sealed class CollectionCache : IDisposable foreach (var conflict in tmpConflicts) { if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0) + || data is IMetaIdentifier meta && conflict.Conflicts.RemoveAll(m => m.Equals(meta)) > 0) AddConflict(data, addedMod, conflict.Mod2); } @@ -374,12 +386,12 @@ public sealed class CollectionCache : IDisposable // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. // Inside the same mod, conflicts are not recorded. - private void AddManipulation(MetaManipulation manip, IMod mod) + private void AddManipulation(IMod mod, IMetaIdentifier identifier, object entry) { - if (!Meta.TryGetValue(manip, out var existingMod)) + if (!Meta.TryGetMod(identifier, out var existingMod)) { - Meta.ApplyMod(manip, mod); - ModData.AddManip(mod, manip); + Meta.ApplyMod(mod, identifier, entry); + ModData.AddManip(mod, identifier); return; } @@ -387,11 +399,11 @@ public sealed class CollectionCache : IDisposable if (mod == existingMod) return; - if (AddConflict(manip, mod, existingMod)) + if (AddConflict(identifier, mod, existingMod)) { - ModData.RemoveManip(existingMod, manip); - Meta.ApplyMod(manip, mod); - ModData.AddManip(mod, manip); + ModData.RemoveManip(existingMod, identifier); + Meta.ApplyMod(mod, identifier, entry); + ModData.AddManip(mod, identifier); } } @@ -437,9 +449,9 @@ public sealed class CollectionCache : IDisposable AddItems(modPath.Mod); } - foreach (var (manip, mod) in Meta) + foreach (var (manip, mod) in Meta.IdentifierSources) { - identifier.MetaChangedItems(items, manip); + manip.AddChangedItems(identifier, items); AddItems(mod); } diff --git a/Penumbra/Collections/Cache/CollectionModData.cs b/Penumbra/Collections/Cache/CollectionModData.cs index d0a3bc76..295191d2 100644 --- a/Penumbra/Collections/Cache/CollectionModData.cs +++ b/Penumbra/Collections/Cache/CollectionModData.cs @@ -9,12 +9,12 @@ namespace Penumbra.Collections.Cache; /// public class CollectionModData { - private readonly Dictionary, HashSet)> _data = new(); + private readonly Dictionary, HashSet)> _data = new(); - public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data - => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); + public IEnumerable<(IMod, IReadOnlySet, IReadOnlySet)> Data + => _data.Select(kvp => (kvp.Key, (IReadOnlySet)kvp.Value.Item1, (IReadOnlySet)kvp.Value.Item2)); - public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) + public (IReadOnlyCollection Paths, IReadOnlyCollection Manipulations) RemoveMod(IMod mod) { if (_data.Remove(mod, out var data)) return data; @@ -35,7 +35,7 @@ public class CollectionModData } } - public void AddManip(IMod mod, MetaManipulation manipulation) + public void AddManip(IMod mod, IMetaIdentifier manipulation) { if (_data.TryGetValue(mod, out var data)) { @@ -54,7 +54,7 @@ public class CollectionModData _data.Remove(mod); } - public void RemoveManip(IMod mod, MetaManipulation manip) + public void RemoveManip(IMod mod, IMetaIdentifier manip) { if (_data.TryGetValue(mod, out var data) && data.Item2.Remove(manip) && data.Item1.Count == 0 && data.Item2.Count == 0) _data.Remove(mod); diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index a0f27c23..f3475c7e 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,5 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui; -using OtterGui.Filesystem; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; @@ -10,28 +10,38 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public readonly struct EqdpCache : IDisposable +public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar - private readonly List _eqdpManipulations = new(); + private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar - public EqdpCache() - { } - - public void SetFiles(MetaFileManager manager) + public override void SetFiles() { for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) - manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); + Manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); } - public void SetFile(MetaFileManager manager, MetaIndex index) + public void SetFile(MetaIndex index) { var i = CharacterUtilityData.EqdpIndices.IndexOf(index); if (i != -1) - manager.SetFile(_eqdpFiles[i], index); + Manager.SetFile(_eqdpFiles[i], index); } - public MetaList.MetaReverter? TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory) + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Eqp); + + protected override void IncorporateChangesInternal() + { + foreach (var (identifier, (_, entry)) in this) + Apply(GetFile(identifier)!, identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQDP manipulations."); + } + + public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) + => _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar + + public MetaList.MetaReverter? TemporarilySetFile(GenderRace genderRace, bool accessory) { var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); if (idx < 0) @@ -47,44 +57,44 @@ public readonly struct EqdpCache : IDisposable return null; } - return manager.TemporarilySetFile(_eqdpFiles[i], idx); + return Manager.TemporarilySetFile(_eqdpFiles[i], idx); } - public void Reset() + public override void Reset() { foreach (var file in _eqdpFiles.OfType()) { var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(_eqdpManipulations.Where(m => m.FileIndex() == relevant).Select(m => (PrimaryId)m.SetId)); + file.Reset(Keys.Where(m => m.FileIndex() == relevant).Select(m => m.SetId)); } - _eqdpManipulations.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip) + protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry) { - _eqdpManipulations.AddOrReplace(manip); - var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??= - new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar - return manip.Apply(file); + if (GetFile(identifier) is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, EqdpManipulation manip) + protected override void RevertModInternal(EqdpIdentifier identifier) { - if (!_eqdpManipulations.Remove(manip)) + if (GetFile(identifier) is { } file) + Apply(file, identifier, ExpandedEqdpFile.GetDefault(Manager, identifier)); + } + + public static bool Apply(ExpandedEqdpFile file, EqdpIdentifier identifier, EqdpEntry entry) + { + var origEntry = file[identifier.SetId]; + var mask = Eqdp.Mask(identifier.Slot); + if ((origEntry & mask) == entry) return false; - var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); - var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!; - manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId); - return manip.Apply(file); + file[identifier.SetId] = (entry & ~mask) | origEntry; + return true; } - public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) - => _eqdpFiles - [Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar - - public void Dispose() + protected override void Dispose(bool _) { for (var i = 0; i < _eqdpFiles.Length; ++i) { @@ -92,6 +102,15 @@ public readonly struct EqdpCache : IDisposable _eqdpFiles[i] = null; } - _eqdpManipulations.Clear(); + Clear(); + } + + private ExpandedEqdpFile? GetFile(EqdpIdentifier identifier) + { + if (!Manager.CharacterUtility.Ready) + return null; + + var index = Array.IndexOf(CharacterUtilityData.EqdpIndices, identifier.FileIndex()); + return _eqdpFiles[index] ??= new ExpandedEqdpFile(Manager, identifier.GenderRace, identifier.Slot.IsAccessory()); } } diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 972ee5a5..599ae588 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,4 +1,4 @@ -using OtterGui.Filesystem; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -7,54 +7,77 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public struct EqpCache : IDisposable +public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedEqpFile? _eqpFile = null; - private readonly List _eqpManipulations = new(); + private ExpandedEqpFile? _eqpFile; - public EqpCache() - { } + public override void SetFiles() + => Manager.SetFile(_eqpFile, MetaIndex.Eqp); - public void SetFiles(MetaFileManager manager) - => manager.SetFile(_eqpFile, MetaIndex.Eqp); + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Eqp); - public static void ResetFiles(MetaFileManager manager) - => manager.SetFile(null, MetaIndex.Eqp); + protected override void IncorporateChangesInternal() + { + if (GetFile() is not { } file) + return; - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); + foreach (var (identifier, (_, entry)) in this) + Apply(file, identifier, entry); - public void Reset() + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations."); + } + + public MetaList.MetaReverter TemporarilySetFile() + => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); + + public override void Reset() { if (_eqpFile == null) return; - _eqpFile.Reset(_eqpManipulations.Select(m => m.SetId)); - _eqpManipulations.Clear(); + _eqpFile.Reset(Keys.Select(identifier => identifier.SetId)); + Clear(); } - public bool ApplyMod(MetaFileManager manager, EqpManipulation manip) + protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry) { - _eqpManipulations.AddOrReplace(manip); - _eqpFile ??= new ExpandedEqpFile(manager); - return manip.Apply(_eqpFile); + if (GetFile() is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, EqpManipulation manip) + protected override void RevertModInternal(EqpIdentifier identifier) { - var idx = _eqpManipulations.FindIndex(manip.Equals); - if (idx < 0) + if (GetFile() is { } file) + Apply(file, identifier, ExpandedEqpFile.GetDefault(Manager, identifier.SetId)); + } + + public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry) + { + var origEntry = file[identifier.SetId]; + var mask = Eqp.Mask(identifier.Slot); + if ((origEntry & mask) == entry) return false; - var def = ExpandedEqpFile.GetDefault(manager, manip.SetId); - manip = new EqpManipulation(def, manip.Slot, manip.SetId); - return manip.Apply(_eqpFile!); + file[identifier.SetId] = (origEntry & ~mask) | entry; + return true; } - public void Dispose() + protected override void Dispose(bool _) { _eqpFile?.Dispose(); _eqpFile = null; - _eqpManipulations.Clear(); + Clear(); + } + + private ExpandedEqpFile? GetFile() + { + if (_eqpFile != null) + return _eqpFile; + + if (!Manager.CharacterUtility.Ready) + return null; + + return _eqpFile = new ExpandedEqpFile(Manager); } } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 3a0b4695..412dd322 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -1,6 +1,3 @@ -using OtterGui.Filesystem; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -9,46 +6,41 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public struct EstCache : IDisposable +public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private EstFile? _estFaceFile = null; - private EstFile? _estHairFile = null; - private EstFile? _estBodyFile = null; - private EstFile? _estHeadFile = null; + private EstFile? _estFaceFile; + private EstFile? _estHairFile; + private EstFile? _estBodyFile; + private EstFile? _estHeadFile; - private readonly List _estManipulations = new(); - - public EstCache() - { } - - public void SetFiles(MetaFileManager manager) + public override void SetFiles() { - manager.SetFile(_estFaceFile, MetaIndex.FaceEst); - manager.SetFile(_estHairFile, MetaIndex.HairEst); - manager.SetFile(_estBodyFile, MetaIndex.BodyEst); - manager.SetFile(_estHeadFile, MetaIndex.HeadEst); + Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); + Manager.SetFile(_estHairFile, MetaIndex.HairEst); + Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); + Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); } - public void SetFile(MetaFileManager manager, MetaIndex index) + public void SetFile(MetaIndex index) { switch (index) { case MetaIndex.FaceEst: - manager.SetFile(_estFaceFile, MetaIndex.FaceEst); + Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); break; case MetaIndex.HairEst: - manager.SetFile(_estHairFile, MetaIndex.HairEst); + Manager.SetFile(_estHairFile, MetaIndex.HairEst); break; case MetaIndex.BodyEst: - manager.SetFile(_estBodyFile, MetaIndex.BodyEst); + Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); break; case MetaIndex.HeadEst: - manager.SetFile(_estHeadFile, MetaIndex.HeadEst); + Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); break; } } - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstType type) + public MetaList.MetaReverter TemporarilySetFiles(EstType type) { var (file, idx) = type switch { @@ -56,74 +48,65 @@ public struct EstCache : IDisposable EstType.Hair => (_estHairFile, MetaIndex.HairEst), EstType.Body => (_estBodyFile, MetaIndex.BodyEst), EstType.Head => (_estHeadFile, MetaIndex.HeadEst), - _ => (null, 0), + _ => (null, 0), }; - return manager.TemporarilySetFile(file, idx); + return Manager.TemporarilySetFile(file, idx); } - private readonly EstFile? GetEstFile(EstType type) + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Eqp); + + protected override void IncorporateChangesInternal() { - return type switch - { - EstType.Face => _estFaceFile, - EstType.Hair => _estHairFile, - EstType.Body => _estBodyFile, - EstType.Head => _estHeadFile, - _ => null, - }; + if (!Manager.CharacterUtility.Ready) + return; + + foreach (var (identifier, (_, entry)) in this) + Apply(GetFile(identifier)!, identifier, entry); + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EST manipulations."); } - internal EstEntry GetEstEntry(MetaFileManager manager, EstType type, GenderRace genderRace, PrimaryId primaryId) + public EstEntry GetEstEntry(EstIdentifier identifier) { - var file = GetEstFile(type); + var file = GetFile(identifier); return file != null - ? file[genderRace, primaryId.Id] - : EstFile.GetDefault(manager, type, genderRace, primaryId); + ? file[identifier.GenderRace, identifier.SetId] + : EstFile.GetDefault(Manager, identifier); } - public void Reset() + public override void Reset() { _estFaceFile?.Reset(); _estHairFile?.Reset(); _estBodyFile?.Reset(); _estHeadFile?.Reset(); - _estManipulations.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, EstManipulation m) + protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry) { - _estManipulations.AddOrReplace(m); - var file = m.Slot switch - { - EstType.Hair => _estHairFile ??= new EstFile(manager, EstType.Hair), - EstType.Face => _estFaceFile ??= new EstFile(manager, EstType.Face), - EstType.Body => _estBodyFile ??= new EstFile(manager, EstType.Body), - EstType.Head => _estHeadFile ??= new EstFile(manager, EstType.Head), - _ => throw new ArgumentOutOfRangeException(), - }; - return m.Apply(file); + if (GetFile(identifier) is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, EstManipulation m) + protected override void RevertModInternal(EstIdentifier identifier) { - if (!_estManipulations.Remove(m)) - return false; - - var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId); - var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def); - var file = m.Slot switch - { - EstType.Hair => _estHairFile!, - EstType.Face => _estFaceFile!, - EstType.Body => _estBodyFile!, - EstType.Head => _estHeadFile!, - _ => throw new ArgumentOutOfRangeException(), - }; - return manip.Apply(file); + if (GetFile(identifier) is { } file) + Apply(file, identifier, EstFile.GetDefault(Manager, identifier.Slot, identifier.GenderRace, identifier.SetId)); } - public void Dispose() + public static bool Apply(EstFile file, EstIdentifier identifier, EstEntry entry) + => file.SetEntry(identifier.GenderRace, identifier.SetId, entry) switch + { + EstFile.EstEntryChange.Unchanged => false, + EstFile.EstEntryChange.Changed => true, + EstFile.EstEntryChange.Added => true, + EstFile.EstEntryChange.Removed => true, + _ => false, + }; + + protected override void Dispose(bool _) { _estFaceFile?.Dispose(); _estHairFile?.Dispose(); @@ -133,6 +116,21 @@ public struct EstCache : IDisposable _estHairFile = null; _estBodyFile = null; _estHeadFile = null; - _estManipulations.Clear(); + Clear(); + } + + private EstFile? GetFile(EstIdentifier identifier) + { + if (Manager.CharacterUtility.Ready) + return null; + + return identifier.Slot switch + { + EstType.Hair => _estHairFile ??= new EstFile(Manager, EstType.Hair), + EstType.Face => _estFaceFile ??= new EstFile(Manager, EstType.Face), + EstType.Body => _estBodyFile ??= new EstFile(Manager, EstType.Body), + EstType.Head => _estHeadFile ??= new EstFile(Manager, EstType.Head), + _ => null, + }; } } diff --git a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs similarity index 75% rename from Penumbra/Meta/Manipulations/GlobalEqpCache.cs rename to Penumbra/Collections/Cache/GlobalEqpCache.cs index 26eb1d05..1c80b47d 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -1,9 +1,11 @@ using OtterGui.Services; using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; -namespace Penumbra.Meta.Manipulations; +namespace Penumbra.Collections.Cache; -public struct GlobalEqpCache : IService +public class GlobalEqpCache : Dictionary, IService { private readonly HashSet _doNotHideEarrings = []; private readonly HashSet _doNotHideNecklace = []; @@ -13,11 +15,9 @@ public struct GlobalEqpCache : IService private bool _doNotHideVieraHats; private bool _doNotHideHrothgarHats; - public GlobalEqpCache() - { } - - public void Clear() + public new void Clear() { + base.Clear(); _doNotHideEarrings.Clear(); _doNotHideNecklace.Clear(); _doNotHideBracelets.Clear(); @@ -29,6 +29,9 @@ public struct GlobalEqpCache : IService public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) { + if (Count == 0) + return original; + if (_doNotHideVieraHats) original |= EqpEntry.HeadShowVieraHat; @@ -52,8 +55,13 @@ public struct GlobalEqpCache : IService return original; } - public bool Add(GlobalEqpManipulation manipulation) - => manipulation.Type switch + public bool ApplyMod(IMod mod, GlobalEqpManipulation manipulation) + { + if (Remove(manipulation, out var oldMod) && oldMod == mod) + return false; + + this[manipulation] = mod; + _ = manipulation.Type switch { GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Add(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Add(manipulation.Condition), @@ -61,12 +69,18 @@ public struct GlobalEqpCache : IService GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Add(manipulation.Condition), GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), - GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), - _ => false, + GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), + _ => false, }; + return true; + } - public bool Remove(GlobalEqpManipulation manipulation) - => manipulation.Type switch + public bool RevertMod(GlobalEqpManipulation manipulation, [NotNullWhen(true)] out IMod? mod) + { + if (!Remove(manipulation, out mod)) + return false; + + _ = manipulation.Type switch { GlobalEqpType.DoNotHideEarrings => _doNotHideEarrings.Remove(manipulation.Condition), GlobalEqpType.DoNotHideNecklace => _doNotHideNecklace.Remove(manipulation.Condition), @@ -74,7 +88,9 @@ public struct GlobalEqpCache : IService GlobalEqpType.DoNotHideRingR => _doNotHideRingR.Remove(manipulation.Condition), GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), - GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), - _ => false, + GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), + _ => false, }; + return true; + } } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 0a713867..1475ffd5 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,4 +1,4 @@ -using OtterGui.Filesystem; +using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Meta; @@ -7,50 +7,76 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; -public struct GmpCache : IDisposable +public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedGmpFile? _gmpFile = null; - private readonly List _gmpManipulations = new(); + private ExpandedGmpFile? _gmpFile; - public GmpCache() - { } + public override void SetFiles() + => Manager.SetFile(_gmpFile, MetaIndex.Gmp); - public void SetFiles(MetaFileManager manager) - => manager.SetFile(_gmpFile, MetaIndex.Gmp); + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.Gmp); - public MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager) - => manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); + protected override void IncorporateChangesInternal() + { + if (GetFile() is not { } file) + return; - public void Reset() + foreach (var (identifier, (_, entry)) in this) + Apply(file, identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed GMP manipulations."); + } + + public MetaList.MetaReverter TemporarilySetFile() + => Manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); + + public override void Reset() { if (_gmpFile == null) return; - _gmpFile.Reset(_gmpManipulations.Select(m => m.SetId)); - _gmpManipulations.Clear(); + _gmpFile.Reset(Keys.Select(identifier => identifier.SetId)); + Clear(); } - public bool ApplyMod(MetaFileManager manager, GmpManipulation manip) + protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry) { - _gmpManipulations.AddOrReplace(manip); - _gmpFile ??= new ExpandedGmpFile(manager); - return manip.Apply(_gmpFile); + if (GetFile() is { } file) + Apply(file, identifier, entry); } - public bool RevertMod(MetaFileManager manager, GmpManipulation manip) + protected override void RevertModInternal(GmpIdentifier identifier) { - if (!_gmpManipulations.Remove(manip)) + if (GetFile() is { } file) + Apply(file, identifier, ExpandedGmpFile.GetDefault(Manager, identifier.SetId)); + } + + public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry) + { + var origEntry = file[identifier.SetId]; + if (entry == origEntry) return false; - var def = ExpandedGmpFile.GetDefault(manager, manip.SetId); - manip = new GmpManipulation(def, manip.SetId); - return manip.Apply(_gmpFile!); + file[identifier.SetId] = entry; + return true; } - public void Dispose() + protected override void Dispose(bool _) { _gmpFile?.Dispose(); _gmpFile = null; - _gmpManipulations.Clear(); + Clear(); + } + + private ExpandedGmpFile? GetFile() + { + if (_gmpFile != null) + return _gmpFile; + + if (!Manager.CharacterUtility.Ready) + return null; + + return _gmpFile = new ExpandedGmpFile(Manager); } } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs new file mode 100644 index 00000000..218c1840 --- /dev/null +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -0,0 +1,89 @@ +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.Collections.Cache; + +public interface IMetaCache : IDisposable +{ + public void SetFiles(); + public void Reset(); + public void ResetFiles(); + + public int Count { get; } +} + +public abstract class MetaCacheBase + : Dictionary, IMetaCache + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + public MetaCacheBase(MetaFileManager manager, ModCollection collection) + { + Manager = manager; + Collection = collection; + if (!Manager.CharacterUtility.Ready) + Manager.CharacterUtility.LoadingFinished += IncorporateChanges; + } + + protected readonly MetaFileManager Manager; + protected readonly ModCollection Collection; + + public void Dispose() + { + Manager.CharacterUtility.LoadingFinished -= IncorporateChanges; + Dispose(true); + } + + public abstract void SetFiles(); + public abstract void Reset(); + public abstract void ResetFiles(); + + public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) + { + lock (this) + { + if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry)) + return false; + + this[identifier] = (source, entry); + } + + ApplyModInternal(identifier, entry); + return true; + } + + public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + { + lock (this) + { + if (!Remove(identifier, out var pair)) + { + mod = null; + return false; + } + + mod = pair.Source; + } + + RevertModInternal(identifier); + return true; + } + + private void IncorporateChanges() + { + lock (this) + { + IncorporateChangesInternal(); + } + if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods) + SetFiles(); + } + + protected abstract void ApplyModInternal(TIdentifier identifier, TEntry entry); + protected abstract void RevertModInternal(TIdentifier identifier); + protected abstract void IncorporateChangesInternal(); + + protected virtual void Dispose(bool _) + { } +} diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 7990122a..3d05e793 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,3 +1,6 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta; using Penumbra.Meta.Files; @@ -6,116 +9,132 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public readonly struct ImcCache : IDisposable +public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary _imcFiles = []; - private readonly List<(ImcManipulation, ImcFile)> _imcManipulations = []; + private readonly Dictionary)> _imcFiles = []; - public ImcCache() - { } + public override void SetFiles() + => SetFiles(false); - public void SetFiles(ModCollection collection, bool fromFullCompute) + public bool GetFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) + { + if (!_imcFiles.TryGetValue(path, out var p)) + { + file = null; + return false; + } + + file = p.Item1; + return true; + } + + public void SetFiles(bool fromFullCompute) { if (fromFullCompute) - foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, collection)); + foreach (var (path, _) in _imcFiles) + Collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, Collection)); else - foreach (var path in _imcFiles.Keys) - collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, collection)); + foreach (var (path, _) in _imcFiles) + Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection)); } - public void Reset(ModCollection collection) + public override void ResetFiles() { - foreach (var (path, file) in _imcFiles) + foreach (var (path, _) in _imcFiles) + Collection._cache!.ForceFile(path, FullPath.Empty); + } + + protected override void IncorporateChangesInternal() + { + if (!Manager.CharacterUtility.Ready) + return; + + foreach (var (identifier, (_, entry)) in this) + ApplyFile(identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed IMC manipulations."); + } + + + public override void Reset() + { + foreach (var (path, (file, set)) in _imcFiles) { - collection._cache!.RemovePath(path); + Collection._cache!.RemovePath(path); file.Reset(); + set.Clear(); } - _imcManipulations.Clear(); + Clear(); } - public bool ApplyMod(MetaFileManager manager, ModCollection collection, ImcManipulation manip) + protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { - if (!manip.Validate(true)) - return false; + if (Manager.CharacterUtility.Ready) + ApplyFile(identifier, entry); + } - var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(manip)); - if (idx < 0) - { - idx = _imcManipulations.Count; - _imcManipulations.Add((manip, null!)); - } - - var path = manip.GamePath(); + private void ApplyFile(ImcIdentifier identifier, ImcEntry entry) + { + var path = identifier.GamePath(); try { - if (!_imcFiles.TryGetValue(path, out var file)) - file = new ImcFile(manager, manip.Identifier); + if (!_imcFiles.TryGetValue(path, out var pair)) + pair = (new ImcFile(Manager, identifier), []); - _imcManipulations[idx] = (manip, file); - if (!manip.Apply(file)) - return false; - _imcFiles[path] = file; - var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); - collection._cache!.ForceFile(path, fullPath); + if (!Apply(pair.Item1, identifier, entry)) + return; - return true; + pair.Item2.Add(identifier); + _imcFiles[path] = pair; + var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); + Collection._cache!.ForceFile(path, fullPath); } catch (ImcException e) { - manager.ValidityChecker.ImcExceptions.Add(e); + Manager.ValidityChecker.ImcExceptions.Add(e); Penumbra.Log.Error(e.ToString()); } catch (Exception e) { - Penumbra.Log.Error($"Could not apply IMC Manipulation {manip}:\n{e}"); + Penumbra.Log.Error($"Could not apply IMC Manipulation {identifier}:\n{e}"); } - - return false; } - public bool RevertMod(MetaFileManager manager, ModCollection collection, ImcManipulation m) + protected override void RevertModInternal(ImcIdentifier identifier) { - if (!m.Validate(false)) - return false; + var path = identifier.GamePath(); + if (!_imcFiles.TryGetValue(path, out var pair)) + return; - var idx = _imcManipulations.FindIndex(p => p.Item1.Equals(m)); - if (idx < 0) - return false; + if (!pair.Item2.Remove(identifier)) + return; - var (_, file) = _imcManipulations[idx]; - _imcManipulations.RemoveAt(idx); - - if (_imcManipulations.All(p => !ReferenceEquals(p.Item2, file))) + if (pair.Item2.Count == 0) { - _imcFiles.Remove(file.Path); - collection._cache!.ForceFile(file.Path, FullPath.Empty); - file.Dispose(); - return true; + _imcFiles.Remove(path); + Collection._cache!.ForceFile(pair.Item1.Path, FullPath.Empty); + pair.Item1.Dispose(); + return; } - var def = ImcFile.GetDefault(manager, file.Path, m.EquipSlot, m.Variant.Id, out _); - var manip = m.Copy(def); - if (!manip.Apply(file)) - return false; + var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _); + if (!Apply(pair.Item1, identifier, def)) + return; - var fullPath = PathDataHandler.CreateImc(file.Path.Path, collection); - collection._cache!.ForceFile(file.Path, fullPath); - - return true; + var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); + Collection._cache!.ForceFile(pair.Item1.Path, fullPath); } - public void Dispose() + public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry) + => file.SetEntry(ImcFile.PartIndex(identifier.EquipSlot), identifier.Variant.Id, entry); + + protected override void Dispose(bool _) { - foreach (var file in _imcFiles.Values) + foreach (var (_, (file, _)) in _imcFiles) file.Dispose(); - + Clear(); _imcFiles.Clear(); - _imcManipulations.Clear(); } - - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) - => _imcFiles.TryGetValue(path, out file); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index fbca9c0e..bc6ef34d 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,3 +1,4 @@ +using System.IO.Pipes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; @@ -9,238 +10,174 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public class MetaCache : IDisposable, IEnumerable> +public class MetaCache(MetaFileManager manager, ModCollection collection) { - private readonly MetaFileManager _manager; - private readonly ModCollection _collection; - private readonly Dictionary _manipulations = new(); - private EqpCache _eqpCache = new(); - private readonly EqdpCache _eqdpCache = new(); - private EstCache _estCache = new(); - private GmpCache _gmpCache = new(); - private CmpCache _cmpCache = new(); - private readonly ImcCache _imcCache = new(); - private GlobalEqpCache _globalEqpCache = new(); - - public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) - { - lock (_manipulations) - { - return _manipulations.TryGetValue(manip, out mod); - } - } + public readonly EqpCache Eqp = new(manager, collection); + public readonly EqdpCache Eqdp = new(manager, collection); + public readonly EstCache Est = new(manager, collection); + public readonly GmpCache Gmp = new(manager, collection); + public readonly RspCache Rsp = new(manager, collection); + public readonly ImcCache Imc = new(manager, collection); + public readonly GlobalEqpCache GlobalEqp = new(); public int Count - => _manipulations.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; - public IReadOnlyCollection Manipulations - => _manipulations.Keys; - - public IEnumerator> GetEnumerator() - => _manipulations.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public MetaCache(MetaFileManager manager, ModCollection collection) - { - _manager = manager; - _collection = collection; - if (!_manager.CharacterUtility.Ready) - _manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations; - } + public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources + => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) + .Concat(Eqdp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Est.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void SetFiles() { - _eqpCache.SetFiles(_manager); - _eqdpCache.SetFiles(_manager); - _estCache.SetFiles(_manager); - _gmpCache.SetFiles(_manager); - _cmpCache.SetFiles(_manager); - _imcCache.SetFiles(_collection, false); + Eqp.SetFiles(); + Eqdp.SetFiles(); + Est.SetFiles(); + Gmp.SetFiles(); + Rsp.SetFiles(); + Imc.SetFiles(false); } public void Reset() { - _eqpCache.Reset(); - _eqdpCache.Reset(); - _estCache.Reset(); - _gmpCache.Reset(); - _cmpCache.Reset(); - _imcCache.Reset(_collection); - _manipulations.Clear(); - _globalEqpCache.Clear(); + Eqp.Reset(); + Eqdp.Reset(); + Est.Reset(); + Gmp.Reset(); + Rsp.Reset(); + Imc.Reset(); + GlobalEqp.Clear(); } public void Dispose() { - _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - _eqpCache.Dispose(); - _eqdpCache.Dispose(); - _estCache.Dispose(); - _gmpCache.Dispose(); - _cmpCache.Dispose(); - _imcCache.Dispose(); - _manipulations.Clear(); + Eqp.Dispose(); + Eqdp.Dispose(); + Est.Dispose(); + Gmp.Dispose(); + Rsp.Dispose(); + Imc.Dispose(); } + public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + { + mod = null; + return identifier switch + { + EqdpIdentifier i => Eqdp.TryGetValue(i, out var p) && Convert(p, out mod), + EqpIdentifier i => Eqp.TryGetValue(i, out var p) && Convert(p, out mod), + EstIdentifier i => Est.TryGetValue(i, out var p) && Convert(p, out mod), + GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod), + ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod), + RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), + GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), + _ => false, + }; + + static bool Convert((IMod, T) pair, out IMod mod) + { + mod = pair.Item1; + return true; + } + } + + public bool RevertMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) + => identifier switch + { + EqdpIdentifier i => Eqdp.RevertMod(i, out mod), + EqpIdentifier i => Eqp.RevertMod(i, out mod), + EstIdentifier i => Est.RevertMod(i, out mod), + GmpIdentifier i => Gmp.RevertMod(i, out mod), + ImcIdentifier i => Imc.RevertMod(i, out mod), + RspIdentifier i => Rsp.RevertMod(i, out mod), + GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), + _ => (mod = null) != null, + }; + + public bool ApplyMod(IMod mod, IMetaIdentifier identifier, object entry) + => identifier switch + { + EqdpIdentifier i when entry is EqdpEntry e => Eqdp.ApplyMod(mod, i, e), + EqdpIdentifier i when entry is EqdpEntryInternal e => Eqdp.ApplyMod(mod, i, e.ToEntry(i.Slot)), + EqpIdentifier i when entry is EqpEntry e => Eqp.ApplyMod(mod, i, e), + EqpIdentifier i when entry is EqpEntryInternal e => Eqp.ApplyMod(mod, i, e.ToEntry(i.Slot)), + EstIdentifier i when entry is EstEntry e => Est.ApplyMod(mod, i, e), + GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e), + ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e), + RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), + GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), + _ => false, + }; + ~MetaCache() => Dispose(); - public bool ApplyMod(MetaManipulation manip, IMod mod) - { - lock (_manipulations) - { - if (_manipulations.ContainsKey(manip)) - _manipulations.Remove(manip); - - _manipulations[manip] = mod; - } - - if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp) - return _globalEqpCache.Add(manip.GlobalEqp); - - if (!_manager.CharacterUtility.Ready) - return true; - - // Imc manipulations do not require character utility, - // but they do require the file space to be ready. - return manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, - }; - } - - public bool RevertMod(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) - { - lock (_manipulations) - { - var ret = _manipulations.Remove(manip, out mod); - - if (manip.ManipulationType is MetaManipulation.Type.GlobalEqp) - return _globalEqpCache.Remove(manip.GlobalEqp); - - if (!_manager.CharacterUtility.Ready) - return ret; - } - - // Imc manipulations do not require character utility, - // but they do require the file space to be ready. - return manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => _eqpCache.RevertMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.RevertMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.RevertMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.RevertMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.RevertMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.RevertMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.Unknown => false, - _ => false, - }; - } - /// Set a single file. public void SetFile(MetaIndex metaIndex) { switch (metaIndex) { case MetaIndex.Eqp: - _eqpCache.SetFiles(_manager); + Eqp.SetFiles(); break; case MetaIndex.Gmp: - _gmpCache.SetFiles(_manager); + Gmp.SetFiles(); break; case MetaIndex.HumanCmp: - _cmpCache.SetFiles(_manager); + Rsp.SetFiles(); break; case MetaIndex.FaceEst: case MetaIndex.HairEst: case MetaIndex.HeadEst: case MetaIndex.BodyEst: - _estCache.SetFile(_manager, metaIndex); + Est.SetFile(metaIndex); break; default: - _eqdpCache.SetFile(_manager, metaIndex); + Eqdp.SetFile(metaIndex); break; } } /// Set the currently relevant IMC files for the collection cache. public void SetImcFiles(bool fromFullCompute) - => _imcCache.SetFiles(_collection, fromFullCompute); + => Imc.SetFiles(fromFullCompute); public MetaList.MetaReverter TemporarilySetEqpFile() - => _eqpCache.TemporarilySetFiles(_manager); + => Eqp.TemporarilySetFile(); public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) - => _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory); + => Eqdp.TemporarilySetFile(genderRace, accessory); public MetaList.MetaReverter TemporarilySetGmpFile() - => _gmpCache.TemporarilySetFiles(_manager); + => Gmp.TemporarilySetFile(); public MetaList.MetaReverter TemporarilySetCmpFile() - => _cmpCache.TemporarilySetFiles(_manager); + => Rsp.TemporarilySetFile(); public MetaList.MetaReverter TemporarilySetEstFile(EstType type) - => _estCache.TemporarilySetFiles(_manager, type); + => Est.TemporarilySetFiles(type); public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) - => _globalEqpCache.Apply(baseEntry, armor); + => GlobalEqp.Apply(baseEntry, armor); /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) - => _imcCache.GetImcFile(path, out file); + => Imc.GetFile(path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) { - var eqdpFile = _eqdpCache.EqdpFile(race, accessory); + var eqdpFile = Eqdp.EqdpFile(race, accessory); if (eqdpFile != null) return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default; - return Meta.Files.ExpandedEqdpFile.GetDefault(_manager, race, accessory, primaryId); + return Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); } internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId) - => _estCache.GetEstEntry(_manager, type, genderRace, primaryId); - - /// Use this when CharacterUtility becomes ready. - private void ApplyStoredManipulations() - { - if (!_manager.CharacterUtility.Ready) - return; - - var loaded = 0; - lock (_manipulations) - { - foreach (var manip in Manipulations) - { - loaded += manip.ManipulationType switch - { - MetaManipulation.Type.Eqp => _eqpCache.ApplyMod(_manager, manip.Eqp), - MetaManipulation.Type.Eqdp => _eqdpCache.ApplyMod(_manager, manip.Eqdp), - MetaManipulation.Type.Est => _estCache.ApplyMod(_manager, manip.Est), - MetaManipulation.Type.Gmp => _gmpCache.ApplyMod(_manager, manip.Gmp), - MetaManipulation.Type.Rsp => _cmpCache.ApplyMod(_manager, manip.Rsp), - MetaManipulation.Type.Imc => _imcCache.ApplyMod(_manager, _collection, manip.Imc), - MetaManipulation.Type.GlobalEqp => false, - MetaManipulation.Type.Unknown => false, - _ => false, - } - ? 1 - : 0; - } - } - - _manager.ApplyDefaultFiles(_collection); - _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; - Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations."); - } + => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace)); } diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs new file mode 100644 index 00000000..3889d6f1 --- /dev/null +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -0,0 +1,81 @@ +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + private CmpFile? _cmpFile; + + public override void SetFiles() + => Manager.SetFile(_cmpFile, MetaIndex.HumanCmp); + + public override void ResetFiles() + => Manager.SetFile(null, MetaIndex.HumanCmp); + + protected override void IncorporateChangesInternal() + { + if (GetFile() is not { } file) + return; + + foreach (var (identifier, (_, entry)) in this) + Apply(file, identifier, entry); + + Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed RSP manipulations."); + } + + public MetaList.MetaReverter TemporarilySetFile() + => Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); + + public override void Reset() + { + if (_cmpFile == null) + return; + + _cmpFile.Reset(Keys.Select(identifier => (identifier.SubRace, identifier.Attribute))); + Clear(); + } + + protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry) + { + if (GetFile() is { } file) + Apply(file, identifier, entry); + } + + protected override void RevertModInternal(RspIdentifier identifier) + { + if (GetFile() is { } file) + Apply(file, identifier, CmpFile.GetDefault(Manager, identifier.SubRace, identifier.Attribute)); + } + + public static bool Apply(CmpFile file, RspIdentifier identifier, RspEntry entry) + { + var value = file[identifier.SubRace, identifier.Attribute]; + if (value == entry) + return false; + + file[identifier.SubRace, identifier.Attribute] = entry; + return true; + } + + protected override void Dispose(bool _) + { + _cmpFile?.Dispose(); + _cmpFile = null; + Clear(); + } + + private CmpFile? GetFile() + { + if (_cmpFile != null) + return _cmpFile; + + if (!Manager.CharacterUtility.Ready) + return null; + + return _cmpFile = new CmpFile(Manager); + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index fdd28ef1..9fa77784 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -37,7 +37,8 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, string outputPath) + public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, + string outputPath) => EnqueueWithResult( new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), action => action.Notifier @@ -52,7 +53,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. /// Modified extra skeleton template parameters. - public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) + public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair[] estManipulations) { var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) @@ -81,20 +82,18 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair[] estManipulations) { // Try to find an EST entry from the manipulations provided. - var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations - .FirstOrNull(est => - est.Gender == gender - && est.Race == race - && est.Slot == type - && est.SetId == info.PrimaryId + .FirstOrNull( + est => est.Key.GenderRace == info.GenderRace + && est.Key.Slot == type + && est.Key.SetId == info.PrimaryId ); // Try to use an entry from provided manipulations, falling back to the current collection. - var targetId = modEst?.Entry + var targetId = modEst?.Value ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? EstEntry.Zero; @@ -102,7 +101,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect if (targetId == EstEntry.Zero) return []; - return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId.AsId)]; + return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. @@ -250,9 +249,11 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect var path = manager.ResolveMtrlPath(relativePath, notifier); if (path == null) return null; + var bytes = read(path); if (bytes == null) return null; + var mtrl = new MtrlFile(bytes); return new MaterialExporter.Material diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index f6157747..1f970dfe 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -13,15 +13,15 @@ public partial class TexToolsMeta private void DeserializeEqpEntry(MetaFileInfo metaFileInfo, byte[]? data) { // Eqp can only be valid for equipment. - if (data == null || !metaFileInfo.EquipSlot.IsEquipment()) + var mask = Eqp.Mask(metaFileInfo.EquipSlot); + if (data == null || mask == 0) return; - var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data); - var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot, - metaFileInfo.PrimaryId); - var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId); - if (_keepDefault || def.Entry != manip.Entry) - MetaManipulations.Add(manip); + var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot); + var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask; + var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask; + if (_keepDefault || def != value) + MetaManipulations.TryAdd(identifier, value); } // Deserialize and check Eqdp Entries and add them to the list if they are non-default. @@ -40,14 +40,12 @@ public partial class TexToolsMeta if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory()) continue; - var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2); - var def = new EqdpManipulation( - ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId), - metaFileInfo.EquipSlot, - gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId); - var manip = new EqdpManipulation(value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId); - if (_keepDefault || def.Entry != manip.Entry) - MetaManipulations.Add(manip); + var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr); + var mask = Eqdp.Mask(metaFileInfo.EquipSlot); + var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask; + var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask; + if (_keepDefault || def != value) + MetaManipulations.TryAdd(identifier, value); } } @@ -57,10 +55,10 @@ public partial class TexToolsMeta if (data == null) return; - var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); - var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); + var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); + var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); if (_keepDefault || value != def) - MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId)); + MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value); } // Deserialize and check Est Entries and add them to the list if they are non-default. @@ -74,7 +72,7 @@ public partial class TexToolsMeta for (var i = 0; i < num; ++i) { var gr = (GenderRace)reader.ReadUInt16(); - var id = reader.ReadUInt16(); + var id = (PrimaryId)reader.ReadUInt16(); var value = new EstEntry(reader.ReadUInt16()); var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch { @@ -87,9 +85,10 @@ public partial class TexToolsMeta if (!gr.IsValid() || type == 0) continue; - var def = EstFile.GetDefault(_metaFileManager, type, gr, id); + var identifier = new EstIdentifier(id, type, gr); + var def = EstFile.GetDefault(_metaFileManager, type, gr, id); if (_keepDefault || def != value) - MetaManipulations.Add(new EstManipulation(gr.Split().Item1, gr.Split().Item2, type, id, value)); + MetaManipulations.TryAdd(identifier, value); } } @@ -107,20 +106,16 @@ public partial class TexToolsMeta ushort i = 0; try { - var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, - metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, - new ImcEntry()); - var def = new ImcFile(_metaFileManager, manip.Identifier); - var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. + var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId, + metaFileInfo.EquipSlot, metaFileInfo.SecondaryType); + var file = new ImcFile(_metaFileManager, identifier); + var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { - if (_keepDefault || !value.Equals(def.GetEntry(partIdx, (Variant)i))) - { - var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, - value); - if (imc.Validate(true)) - MetaManipulations.Add(imc); - } + identifier = identifier with { Variant = (Variant)i }; + var def = file.GetEntry(partIdx, (Variant)i); + if (_keepDefault || def != value && identifier.Validate()) + MetaManipulations.TryAdd(identifier, value); ++i; } diff --git a/Penumbra/Import/TexToolsMeta.Export.cs b/Penumbra/Import/TexToolsMeta.Export.cs index 4fb56df6..9cce60e3 100644 --- a/Penumbra/Import/TexToolsMeta.Export.cs +++ b/Penumbra/Import/TexToolsMeta.Export.cs @@ -1,3 +1,4 @@ +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -8,7 +9,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { - public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable manipulations, DirectoryInfo basePath) + public static void WriteTexToolsMeta(MetaFileManager manager, MetaDictionary manipulations, DirectoryInfo basePath) { var files = ConvertToTexTools(manager, manipulations); @@ -27,49 +28,81 @@ public partial class TexToolsMeta } } - public static Dictionary ConvertToTexTools(MetaFileManager manager, IEnumerable manips) + public static Dictionary ConvertToTexTools(MetaFileManager manager, MetaDictionary manips) { var ret = new Dictionary(); - foreach (var group in manips.GroupBy(ManipToPath)) + foreach (var group in manips.Rsp.GroupBy(ManipToPath)) { if (group.Key.Length == 0) continue; - var bytes = group.Key.EndsWith(".rgsp") - ? WriteRgspFile(manager, group.Key, group) - : WriteMetaFile(manager, group.Key, group); + var bytes = WriteRgspFile(manager, group); if (bytes.Length == 0) continue; ret.Add(group.Key, bytes); } + foreach (var (file, dict) in SplitByFile(manips)) + { + var bytes = WriteMetaFile(manager, file, dict); + if (bytes.Length == 0) + continue; + + ret.Add(file, bytes); + } + return ret; } - private static byte[] WriteRgspFile(MetaFileManager manager, string path, IEnumerable manips) + private static Dictionary SplitByFile(MetaDictionary manips) { - var list = manips.GroupBy(m => m.Rsp.Attribute).ToDictionary(m => m.Key, m => m.Last().Rsp); + var ret = new Dictionary(); + foreach (var (identifier, key) in manips.Imc) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Eqp) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Eqdp) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Est) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + foreach (var (identifier, key) in manips.Gmp) + GetDict(ManipToPath(identifier)).TryAdd(identifier, key); + + ret.Remove(string.Empty); + + return ret; + + MetaDictionary GetDict(string path) + { + if (!ret.TryGetValue(path, out var dict)) + { + dict = new MetaDictionary(); + ret.Add(path, dict); + } + + return dict; + } + } + + private static byte[] WriteRgspFile(MetaFileManager manager, IEnumerable> manips) + { + var list = manips.GroupBy(m => m.Key.Attribute).ToDictionary(g => g.Key, g => g.Last()); using var m = new MemoryStream(45); using var b = new BinaryWriter(m); // Version b.Write(byte.MaxValue); b.Write((ushort)2); - var race = list.First().Value.SubRace; - var gender = list.First().Value.Attribute.ToGender(); + var race = list.First().Value.Key.SubRace; + var gender = list.First().Value.Key.Attribute.ToGender(); b.Write((byte)(race - 1)); // offset by one due to Unknown b.Write((byte)(gender - 1)); // offset by one due to Unknown - void Add(params RspAttribute[] attributes) - { - foreach (var attribute in attributes) - { - var value = list.TryGetValue(attribute, out var tmp) ? tmp.Entry : CmpFile.GetDefault(manager, race, attribute); - b.Write(value.Value); - } - } - if (gender == Gender.Male) { Add(RspAttribute.MaleMinSize, RspAttribute.MaleMaxSize, RspAttribute.MaleMinTail, RspAttribute.MaleMaxTail); @@ -82,12 +115,24 @@ public partial class TexToolsMeta } return m.GetBuffer(); + + void Add(params RspAttribute[] attributes) + { + foreach (var attribute in attributes) + { + var value = list.TryGetValue(attribute, out var tmp) ? tmp.Value : CmpFile.GetDefault(manager, race, attribute); + b.Write(value.Value); + } + } } - private static byte[] WriteMetaFile(MetaFileManager manager, string path, IEnumerable manips) + private static byte[] WriteMetaFile(MetaFileManager manager, string path, MetaDictionary manips) { - var filteredManips = manips.GroupBy(m => m.ManipulationType).ToDictionary(p => p.Key, p => p.Select(x => x)); - + var headerCount = (manips.Imc.Count > 0 ? 1 : 0) + + (manips.Eqp.Count > 0 ? 1 : 0) + + (manips.Eqdp.Count > 0 ? 1 : 0) + + (manips.Est.Count > 0 ? 1 : 0) + + (manips.Gmp.Count > 0 ? 1 : 0); using var m = new MemoryStream(); using var b = new BinaryWriter(m); @@ -101,7 +146,7 @@ public partial class TexToolsMeta b.Write((byte)0); // Number of Headers - b.Write((uint)filteredManips.Count); + b.Write((uint)headerCount); // Current TT Size of Headers b.Write((uint)12); @@ -109,88 +154,44 @@ public partial class TexToolsMeta var headerStart = b.BaseStream.Position + 4; b.Write((uint)headerStart); - var offset = (uint)(b.BaseStream.Position + 12 * filteredManips.Count); - foreach (var (header, data) in filteredManips) - { - b.Write((uint)header); - b.Write(offset); - - var size = WriteData(manager, b, offset, header, data); - b.Write(size); - offset += size; - } + var offset = (uint)(b.BaseStream.Position + 12 * manips.Count); + offset += WriteData(manager, b, offset, manips.Imc); + offset += WriteData(b, offset, manips.Eqdp); + offset += WriteData(b, offset, manips.Eqp); + offset += WriteData(b, offset, manips.Est); + offset += WriteData(b, offset, manips.Gmp); return m.ToArray(); } - private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, - IEnumerable manips) + private static uint WriteData(MetaFileManager manager, BinaryWriter b, uint offset, IReadOnlyDictionary manips) { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + var oldPos = b.BaseStream.Position; b.Seek((int)offset, SeekOrigin.Begin); - switch (type) + var refIdentifier = manips.First().Key; + var baseFile = new ImcFile(manager, refIdentifier); + foreach (var (identifier, entry) in manips) + ImcCache.Apply(baseFile, identifier, entry); + + var partIdx = refIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory + ? ImcFile.PartIndex(refIdentifier.EquipSlot) + : 0; + + for (var i = 0; i <= baseFile.Count; ++i) { - case MetaManipulation.Type.Imc: - var allManips = manips.ToList(); - var baseFile = new ImcFile(manager, allManips[0].Imc.Identifier); - foreach (var manip in allManips) - manip.Imc.Apply(baseFile); - - var partIdx = allManips[0].Imc.ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? ImcFile.PartIndex(allManips[0].Imc.EquipSlot) - : 0; - - for (var i = 0; i <= baseFile.Count; ++i) - { - var entry = baseFile.GetEntry(partIdx, (Variant)i); - b.Write(entry.MaterialId); - b.Write(entry.DecalId); - b.Write(entry.AttributeAndSound); - b.Write(entry.VfxId); - b.Write(entry.MaterialAnimationId); - } - - break; - case MetaManipulation.Type.Eqdp: - foreach (var manip in manips) - { - b.Write((uint)Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race)); - var entry = (byte)(((uint)manip.Eqdp.Entry >> Eqdp.Offset(manip.Eqdp.Slot)) & 0x03); - b.Write(entry); - } - - break; - case MetaManipulation.Type.Eqp: - foreach (var manip in manips) - { - var bytes = BitConverter.GetBytes((ulong)manip.Eqp.Entry); - var (numBytes, byteOffset) = Eqp.BytesAndOffset(manip.Eqp.Slot); - for (var i = byteOffset; i < numBytes + byteOffset; ++i) - b.Write(bytes[i]); - } - - break; - case MetaManipulation.Type.Est: - foreach (var manip in manips) - { - b.Write((ushort)Names.CombinedRace(manip.Est.Gender, manip.Est.Race)); - b.Write(manip.Est.SetId.Id); - b.Write(manip.Est.Entry.Value); - } - - break; - case MetaManipulation.Type.Gmp: - foreach (var manip in manips) - { - b.Write((uint)manip.Gmp.Entry.Value); - b.Write(manip.Gmp.Entry.UnknownTotal); - } - - break; - case MetaManipulation.Type.GlobalEqp: - // Not Supported - break; + var entry = baseFile.GetEntry(partIdx, (Variant)i); + b.Write(entry.MaterialId); + b.Write(entry.DecalId); + b.Write(entry.AttributeAndSound); + b.Write(entry.VfxId); + b.Write(entry.MaterialAnimationId); } var size = b.BaseStream.Position - offset; @@ -198,19 +199,98 @@ public partial class TexToolsMeta return (uint)size; } - private static string ManipToPath(MetaManipulation manip) - => manip.ManipulationType switch - { - MetaManipulation.Type.Imc => ManipToPath(manip.Imc), - MetaManipulation.Type.Eqdp => ManipToPath(manip.Eqdp), - MetaManipulation.Type.Eqp => ManipToPath(manip.Eqp), - MetaManipulation.Type.Est => ManipToPath(manip.Est), - MetaManipulation.Type.Gmp => ManipToPath(manip.Gmp), - MetaManipulation.Type.Rsp => ManipToPath(manip.Rsp), - _ => string.Empty, - }; + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; - private static string ManipToPath(ImcManipulation manip) + b.Write((uint)MetaManipulationType.Eqdp); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var (identifier, entry) in manips) + { + b.Write((uint)identifier.GenderRace); + b.Write(entry.AsByte); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, + IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var (identifier, entry) in manips) + { + var numBytes = Eqp.BytesAndOffset(identifier.Slot).Item1; + for (var i = 0; i < numBytes; ++i) + b.Write((byte)(entry.Value >> (8 * i))); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var (identifier, entry) in manips) + { + b.Write((ushort)identifier.GenderRace); + b.Write(identifier.SetId.Id); + b.Write(entry.Value); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static uint WriteData(BinaryWriter b, uint offset, IReadOnlyDictionary manips) + { + if (manips.Count == 0) + return 0; + + b.Write((uint)MetaManipulationType.Imc); + b.Write(offset); + + var oldPos = b.BaseStream.Position; + b.Seek((int)offset, SeekOrigin.Begin); + + foreach (var entry in manips.Values) + { + b.Write((uint)entry.Value); + b.Write(entry.UnknownTotal); + } + + var size = b.BaseStream.Position - offset; + b.Seek((int)oldPos, SeekOrigin.Begin); + return (uint)size; + } + + private static string ManipToPath(ImcIdentifier manip) { var path = manip.GamePath().ToString(); var replacement = manip.ObjectType switch @@ -224,33 +304,33 @@ public partial class TexToolsMeta return path.Replace(".imc", replacement); } - private static string ManipToPath(EqdpManipulation manip) + private static string ManipToPath(EqdpIdentifier manip) => manip.Slot.IsAccessory() - ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" - : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + ? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"; - private static string ManipToPath(EqpManipulation manip) + private static string ManipToPath(EqpIdentifier manip) => manip.Slot.IsAccessory() - ? $"chara/accessory/a{manip.SetId:D4}/a{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta" - : $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{manip.Slot.ToSuffix()}.meta"; + ? $"chara/accessory/a{manip.SetId.Id:D4}/a{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta" + : $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{manip.Slot.ToSuffix()}.meta"; - private static string ManipToPath(EstManipulation manip) + private static string ManipToPath(EstIdentifier manip) { var raceCode = Names.CombinedRace(manip.Gender, manip.Race).ToRaceCode(); return manip.Slot switch { - EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId:D4}/c{raceCode}h{manip.SetId:D4}_hir.meta", - EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId:D4}/c{raceCode}f{manip.SetId:D4}_fac.meta", - EstType.Body => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Body.ToSuffix()}.meta", - EstType.Head => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta", - _ => throw new ArgumentOutOfRangeException(), + EstType.Hair => $"chara/human/c{raceCode}/obj/hair/h{manip.SetId.Id:D4}/c{raceCode}h{manip.SetId.Id:D4}_hir.meta", + EstType.Face => $"chara/human/c{raceCode}/obj/face/h{manip.SetId.Id:D4}/c{raceCode}f{manip.SetId.Id:D4}_fac.meta", + EstType.Body => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Body.ToSuffix()}.meta", + EstType.Head => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta", + _ => throw new ArgumentOutOfRangeException(), }; } - private static string ManipToPath(GmpManipulation manip) - => $"chara/equipment/e{manip.SetId:D4}/e{manip.SetId:D4}_{EquipSlot.Head.ToSuffix()}.meta"; + private static string ManipToPath(GmpIdentifier manip) + => $"chara/equipment/e{manip.SetId.Id:D4}/e{manip.SetId.Id:D4}_{EquipSlot.Head.ToSuffix()}.meta"; - private static string ManipToPath(RspManipulation manip) - => $"chara/xls/charamake/rgsp/{(int)manip.SubRace - 1}-{(int)manip.Attribute.ToGender() - 1}.rgsp"; + private static string ManipToPath(KeyValuePair manip) + => $"chara/xls/charamake/rgsp/{(int)manip.Key.SubRace - 1}-{(int)manip.Key.Attribute.ToGender() - 1}.rgsp"; } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 71b9165f..7b0bb5a8 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -42,14 +42,6 @@ public partial class TexToolsMeta return Invalid; } - // Add the given values to the manipulations if they are not default. - void Add(RspAttribute attribute, float value) - { - var def = CmpFile.GetDefault(manager, subRace, attribute); - if (keepDefault || value != def.Value) - ret.MetaManipulations.Add(new RspManipulation(subRace, attribute, new RspEntry(value))); - } - if (gender == 1) { Add(RspAttribute.FemaleMinSize, br.ReadSingle()); @@ -73,5 +65,14 @@ public partial class TexToolsMeta } return ret; + + // Add the given values to the manipulations if they are not default. + void Add(RspAttribute attribute, float value) + { + var identifier = new RspIdentifier(subRace, attribute); + var def = CmpFile.GetDefault(manager, subRace, attribute); + if (keepDefault || value != def.Value) + ret.MetaManipulations.TryAdd(identifier, new RspEntry(value)); + } } } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index 25e00bd7..c4a8e81f 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -1,4 +1,3 @@ -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Import.Structs; using Penumbra.Meta; @@ -22,10 +21,10 @@ public partial class TexToolsMeta public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0); // The info class determines the files or table locations the changes need to apply to from the filename. - public readonly uint Version; - public readonly string FilePath; - public readonly List MetaManipulations = new(); - private readonly bool _keepDefault = false; + public readonly uint Version; + public readonly string FilePath; + public readonly MetaDictionary MetaManipulations = new(); + private readonly bool _keepDefault; private readonly MetaFileManager _metaFileManager; @@ -44,18 +43,18 @@ public partial class TexToolsMeta var headerStart = reader.ReadUInt32(); reader.BaseStream.Seek(headerStart, SeekOrigin.Begin); - List<(MetaManipulation.Type type, uint offset, int size)> entries = []; + List<(MetaManipulationType type, uint offset, int size)> entries = []; for (var i = 0; i < numHeaders; ++i) { var currentOffset = reader.BaseStream.Position; - var type = (MetaManipulation.Type)reader.ReadUInt32(); + var type = (MetaManipulationType)reader.ReadUInt32(); var offset = reader.ReadUInt32(); var size = reader.ReadInt32(); entries.Add((type, offset, size)); reader.BaseStream.Seek(currentOffset + headerSize, SeekOrigin.Begin); } - byte[]? ReadEntry(MetaManipulation.Type type) + byte[]? ReadEntry(MetaManipulationType type) { var idx = entries.FindIndex(t => t.type == type); if (idx < 0) @@ -65,11 +64,11 @@ public partial class TexToolsMeta return reader.ReadBytes(entries[idx].size); } - DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Eqp)); - DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulation.Type.Gmp)); - DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulation.Type.Eqdp)); - DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulation.Type.Est)); - DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulation.Type.Imc)); + DeserializeEqpEntry(metaInfo, ReadEntry(MetaManipulationType.Eqp)); + DeserializeGmpEntry(metaInfo, ReadEntry(MetaManipulationType.Gmp)); + DeserializeEqdpEntries(metaInfo, ReadEntry(MetaManipulationType.Eqdp)); + DeserializeEstEntries(metaInfo, ReadEntry(MetaManipulationType.Est)); + DeserializeImcEntries(metaInfo, ReadEntry(MetaManipulationType.Imc)); } catch (Exception e) { diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index b42571ac..bc474952 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,7 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using Microsoft.VisualBasic; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 2b87e688..4dfefd96 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -273,7 +273,7 @@ internal partial record ResolveContext { var metaCache = Global.Collection.MetaCache; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; - return (raceCode, EstManipulation.ToName(type), skeletonSet.AsId); + return (raceCode, type.ToName(), skeletonSet.AsId); } private unsafe Utf8GamePath ResolveSkeletonPathNative(uint partialSkeletonIndex) diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 650919a3..751113a0 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -51,10 +51,6 @@ public class ImcChecker return entry; } - public CachedEntry GetDefaultEntry(ImcManipulation imcManip, bool storeCache) - => GetDefaultEntry(new ImcIdentifier(imcManip.PrimaryId, imcManip.Variant, imcManip.ObjectType, imcManip.SecondaryId.Id, - imcManip.EquipSlot, imcManip.BodySlot), storeCache); - private static ImcFile? GetFile(ImcIdentifier identifier) { if (_dataManager == null) diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 6306f419..3f856bd2 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -69,10 +69,16 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Eqdp; } public readonly record struct EqdpEntryInternal(bool Material, bool Model) { + public byte AsByte + => (byte)(Material ? Model ? 3 : 1 : Model ? 2 : 0); + private EqdpEntryInternal((bool, bool) val) : this(val.Item1, val.Item2) { } @@ -83,4 +89,7 @@ public readonly record struct EqdpEntryInternal(bool Material, bool Model) public EqdpEntry ToEntry(EquipSlot slot) => Eqdp.FromSlotAndBits(slot, Material, Model); + + public override string ToString() + => $"Material: {Material}, Model: {Model}"; } diff --git a/Penumbra/Meta/Manipulations/EqdpManipulation.cs b/Penumbra/Meta/Manipulations/EqdpManipulation.cs deleted file mode 100644 index 8c5f27e5..00000000 --- a/Penumbra/Meta/Manipulations/EqdpManipulation.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqdpManipulation(EqdpIdentifier identifier, EqdpEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public EqdpIdentifier Identifier { get; } = identifier; - - public EqdpEntry Entry { get; } = entry; - - [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender - => Identifier.Gender; - - [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race - => Identifier.Race; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot - => Identifier.Slot; - - [JsonConstructor] - public EqdpManipulation(EqdpEntry entry, EquipSlot slot, Gender gender, ModelRace race, PrimaryId setId) - : this(new EqdpIdentifier(setId, slot, Names.CombinedRace(gender, race)), Eqdp.Mask(slot) & entry) - { } - - public EqdpManipulation Copy(EqdpManipulation entry) - { - if (entry.Slot != Slot) - { - var (bit1, bit2) = entry.Entry.ToBits(entry.Slot); - return new EqdpManipulation(Identifier, Eqdp.FromSlotAndBits(Slot, bit1, bit2)); - } - - return new EqdpManipulation(Identifier, entry.Entry); - } - - public EqdpManipulation Copy(EqdpEntry entry) - => new(entry, Slot, Gender, Race, SetId); - - public override string ToString() - => $"Eqdp - {SetId} - {Slot} - {Race.ToName()} - {Gender.ToName()}"; - - public bool Equals(EqdpManipulation other) - => Gender == other.Gender - && Race == other.Race - && SetId == other.SetId - && Slot == other.Slot; - - public override bool Equals(object? obj) - => obj is EqdpManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot); - - public int CompareTo(EqdpManipulation other) - { - var r = Race.CompareTo(other.Race); - if (r != 0) - return r; - - var g = Gender.CompareTo(other.Gender); - if (g != 0) - return g; - - var set = SetId.Id.CompareTo(other.SetId.Id); - return set != 0 ? set : Slot.CompareTo(other.Slot); - } - - public MetaIndex FileIndex() - => CharacterUtilityData.EqdpIdx(Names.CombinedRace(Gender, Race), Slot.IsAccessory()); - - public bool Apply(ExpandedEqdpFile file) - { - var entry = file[SetId]; - var mask = Eqdp.Mask(Slot); - if ((entry & mask) == Entry) - return false; - - file[SetId] = (entry & ~mask) | Entry; - return true; - } - - public bool Validate() - { - var mask = Eqdp.Mask(Slot); - if (mask == 0) - return false; - - if ((mask & Entry) != Entry) - return false; - - if (FileIndex() == (MetaIndex)(-1)) - return false; - - // No check for set id. - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 572dc203..ec4dd6e7 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -50,6 +50,9 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Eqp; } public readonly record struct EqpEntryInternal(uint Value) diff --git a/Penumbra/Meta/Manipulations/EqpManipulation.cs b/Penumbra/Meta/Manipulations/EqpManipulation.cs deleted file mode 100644 index eef21d12..00000000 --- a/Penumbra/Meta/Manipulations/EqpManipulation.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.Util; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EqpManipulation(EqpIdentifier identifier, EqpEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public EqpIdentifier Identifier { get; } = identifier; - - [JsonConverter(typeof(ForceNumericFlagEnumConverter))] - public EqpEntry Entry { get; } = entry; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot Slot - => Identifier.Slot; - - [JsonConstructor] - public EqpManipulation(EqpEntry entry, EquipSlot slot, PrimaryId setId) - : this(new EqpIdentifier(setId, slot), Eqp.Mask(slot) & entry) - { } - - public EqpManipulation Copy(EqpEntry entry) - => new(Identifier, entry); - - public override string ToString() - => $"Eqp - {SetId} - {Slot}"; - - public bool Equals(EqpManipulation other) - => Slot == other.Slot - && SetId == other.SetId; - - public override bool Equals(object? obj) - => obj is EqpManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)Slot, SetId); - - public int CompareTo(EqpManipulation other) - { - var set = SetId.Id.CompareTo(other.SetId.Id); - return set != 0 ? set : Slot.CompareTo(other.Slot); - } - - public MetaIndex FileIndex() - => MetaIndex.Eqp; - - public bool Apply(ExpandedEqpFile file) - { - var entry = file[SetId]; - var mask = Eqp.Mask(Slot); - if ((entry & mask) == Entry) - return false; - - file[SetId] = (entry & ~mask) | Entry; - return true; - } - - public bool Validate() - { - var mask = Eqp.Mask(Slot); - if (mask == 0) - return false; - if ((Entry & mask) != Entry) - return false; - - // No check for set id. - - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 9f878f97..2955dba4 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -91,6 +91,9 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende jObj["Slot"] = Slot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Est; } [JsonConverter(typeof(Converter))] @@ -111,3 +114,16 @@ public readonly record struct EstEntry(ushort Value) => new(serializer.Deserialize(reader)); } } + +public static class EstTypeExtension +{ + public static string ToName(this EstType type) + => type switch + { + EstType.Hair => "hair", + EstType.Face => "face", + EstType.Body => "top", + EstType.Head => "met", + _ => "unk", + }; +} diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs deleted file mode 100644 index 09abbaa5..00000000 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct EstManipulation(EstIdentifier identifier, EstEntry entry) : IMetaManipulation -{ - public static string ToName(EstType type) - => type switch - { - EstType.Hair => "hair", - EstType.Face => "face", - EstType.Body => "top", - EstType.Head => "met", - _ => "unk", - }; - - [JsonIgnore] - public EstIdentifier Identifier { get; } = identifier; - public EstEntry Entry { get; } = entry; - - [JsonConverter(typeof(StringEnumConverter))] - public Gender Gender - => Identifier.Gender; - - [JsonConverter(typeof(StringEnumConverter))] - public ModelRace Race - => Identifier.Race; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConverter(typeof(StringEnumConverter))] - public EstType Slot - => Identifier.Slot; - - - [JsonConstructor] - public EstManipulation(Gender gender, ModelRace race, EstType slot, PrimaryId setId, EstEntry entry) - : this(new EstIdentifier(setId, slot, Names.CombinedRace(gender, race)), entry) - { } - - public EstManipulation Copy(EstEntry entry) - => new(Identifier, entry); - - - public override string ToString() - => $"Est - {SetId} - {Slot} - {Race.ToName()} {Gender.ToName()}"; - - public bool Equals(EstManipulation other) - => Gender == other.Gender - && Race == other.Race - && SetId == other.SetId - && Slot == other.Slot; - - public override bool Equals(object? obj) - => obj is EstManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)Gender, (int)Race, SetId, (int)Slot); - - public int CompareTo(EstManipulation other) - { - var r = Race.CompareTo(other.Race); - if (r != 0) - return r; - - var g = Gender.CompareTo(other.Gender); - if (g != 0) - return g; - - var s = Slot.CompareTo(other.Slot); - return s != 0 ? s : SetId.Id.CompareTo(other.SetId.Id); - } - - public MetaIndex FileIndex() - => (MetaIndex)Slot; - - public bool Apply(EstFile file) - { - return file.SetEntry(Names.CombinedRace(Gender, Race), SetId.Id, Entry) switch - { - EstFile.EstEntryChange.Unchanged => false, - EstFile.EstEntryChange.Changed => true, - EstFile.EstEntryChange.Added => true, - EstFile.EstEntryChange.Removed => true, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public bool Validate() - { - if (!Enum.IsDefined(Slot)) - return false; - if (Names.CombinedRace(Gender, Race) == GenderRace.Unknown) - return false; - - // No known check for set id or entry. - return true; - } -} - - diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 94c892e2..2b88d962 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -1,11 +1,12 @@ using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly struct GlobalEqpManipulation : IMetaManipulation, IMetaIdentifier +public readonly struct GlobalEqpManipulation : IMetaIdentifier { public GlobalEqpType Type { get; init; } public PrimaryId Condition { get; init; } @@ -70,8 +71,29 @@ public readonly struct GlobalEqpManipulation : IMetaManipulation $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - { } + { + var path = Type switch + { + GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Ears), + GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Neck), + GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists), + GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger), + GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger), + GlobalEqpType.DoNotHideHrothgarHats => string.Empty, + GlobalEqpType.DoNotHideVieraHats => string.Empty, + _ => string.Empty, + }; + if (path.Length > 0) + identifier.Identify(changedItems, path); + else if (Type is GlobalEqpType.DoNotHideVieraHats) + changedItems["All Hats for Viera"] = null; + else if (Type is GlobalEqpType.DoNotHideHrothgarHats) + changedItems["All Hats for Hrothgar"] = null; + } public MetaIndex FileIndex() => MetaIndex.Eqp; + + MetaManipulationType IMetaIdentifier.Type + => MetaManipulationType.GlobalEqp; } diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 1b7c70ba..a6fcf58b 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -36,4 +36,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, jObj["SetId"] = SetId.Id.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Gmp; } diff --git a/Penumbra/Meta/Manipulations/GmpManipulation.cs b/Penumbra/Meta/Manipulations/GmpManipulation.cs deleted file mode 100644 index 431f6325..00000000 --- a/Penumbra/Meta/Manipulations/GmpManipulation.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct GmpManipulation(GmpIdentifier identifier, GmpEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public GmpIdentifier Identifier { get; } = identifier; - - public GmpEntry Entry { get; } = entry; - - public PrimaryId SetId - => Identifier.SetId; - - [JsonConstructor] - public GmpManipulation(GmpEntry entry, PrimaryId setId) - : this(new GmpIdentifier(setId), entry) - { } - - public GmpManipulation Copy(GmpEntry entry) - => new(Identifier, entry); - - public override string ToString() - => $"Gmp - {SetId}"; - - public bool Equals(GmpManipulation other) - => SetId == other.SetId; - - public override bool Equals(object? obj) - => obj is GmpManipulation other && Equals(other); - - public override int GetHashCode() - => SetId.GetHashCode(); - - public int CompareTo(GmpManipulation other) - => SetId.Id.CompareTo(other.SetId.Id); - - public MetaIndex FileIndex() - => MetaIndex.Gmp; - - public bool Apply(ExpandedGmpFile file) - { - var entry = file[SetId]; - if (entry == Entry) - return false; - - file[SetId] = Entry; - return true; - } - - public bool Validate() - // No known conditions. - => true; -} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 4ad6bd3d..5707ffca 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -4,6 +4,18 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; +public enum MetaManipulationType : byte +{ + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + Rsp = 6, + GlobalEqp = 7, +} + public interface IMetaIdentifier { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); @@ -13,4 +25,8 @@ public interface IMetaIdentifier public bool Validate(); public JObject AddToJson(JObject jObj); + + public MetaManipulationType Type { get; } + + public string ToString(); } diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 2a2f4c03..44c60942 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,9 +27,6 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public ImcManipulation ToManipulation(ImcEntry entry) - => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, entry); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); @@ -193,4 +190,7 @@ public readonly record struct ImcIdentifier( jObj["BodySlot"] = BodySlot.ToString(); return jObj; } + + public MetaManipulationType Type + => MetaManipulationType.Imc; } diff --git a/Penumbra/Meta/Manipulations/ImcManipulation.cs b/Penumbra/Meta/Manipulations/ImcManipulation.cs deleted file mode 100644 index 5065a06e..00000000 --- a/Penumbra/Meta/Manipulations/ImcManipulation.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; -using Penumbra.String.Classes; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct ImcManipulation : IMetaManipulation -{ - [JsonIgnore] - public ImcIdentifier Identifier { get; private init; } - - public ImcEntry Entry { get; private init; } - - - public PrimaryId PrimaryId - => Identifier.PrimaryId; - - public SecondaryId SecondaryId - => Identifier.SecondaryId; - - public Variant Variant - => Identifier.Variant; - - [JsonConverter(typeof(StringEnumConverter))] - public ObjectType ObjectType - => Identifier.ObjectType; - - [JsonConverter(typeof(StringEnumConverter))] - public EquipSlot EquipSlot - => Identifier.EquipSlot; - - [JsonConverter(typeof(StringEnumConverter))] - public BodySlot BodySlot - => Identifier.BodySlot; - - public ImcManipulation(EquipSlot equipSlot, ushort variant, PrimaryId primaryId, ImcEntry entry) - : this(new ImcIdentifier(equipSlot, primaryId, variant), entry) - { } - - public ImcManipulation(ImcIdentifier identifier, ImcEntry entry) - { - Identifier = identifier; - Entry = entry; - } - - - // Variants were initially ushorts but got shortened to bytes. - // There are still some manipulations around that have values > 255 for variant, - // so we change the unused value to something nonsensical in that case, just so they do not compare equal, - // and clamp the variant to 255. - [JsonConstructor] - internal ImcManipulation(ObjectType objectType, BodySlot bodySlot, PrimaryId primaryId, SecondaryId secondaryId, ushort variant, - EquipSlot equipSlot, ImcEntry entry) - { - Entry = entry; - var v = (Variant)Math.Clamp(variant, (ushort)0, byte.MaxValue); - Identifier = objectType switch - { - ObjectType.Accessory or ObjectType.Equipment => new ImcIdentifier(primaryId, v, objectType, 0, equipSlot, - variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - ObjectType.DemiHuman => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, variant > byte.MaxValue ? BodySlot.Body : BodySlot.Unknown), - _ => new ImcIdentifier(primaryId, v, objectType, secondaryId, equipSlot, bodySlot == BodySlot.Unknown ? BodySlot.Body : BodySlot.Unknown), - }; - } - - public ImcManipulation Copy(ImcEntry entry) - => new(Identifier, entry); - - public override string ToString() - => Identifier.ToString(); - - public bool Equals(ImcManipulation other) - => Identifier == other.Identifier; - - public override bool Equals(object? obj) - => obj is ImcManipulation other && Equals(other); - - public override int GetHashCode() - => Identifier.GetHashCode(); - - public int CompareTo(ImcManipulation other) - => Identifier.CompareTo(other.Identifier); - - public MetaIndex FileIndex() - => Identifier.FileIndex(); - - public Utf8GamePath GamePath() - => Identifier.GamePath(); - - public bool Apply(ImcFile file) - => file.SetEntry(ImcFile.PartIndex(EquipSlot), Variant.Id, Entry); - - public bool Validate(bool withMaterial) - { - if (!Identifier.Validate()) - return false; - - if (withMaterial && Entry.MaterialId == 0) - return false; - - return true; - } -} diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 236157ae..5a51df83 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -7,7 +8,7 @@ using ImcEntry = Penumbra.GameData.Structs.ImcEntry; namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] -public class MetaDictionary : IEnumerable +public class MetaDictionary { private readonly Dictionary _imc = []; private readonly Dictionary _eqp = []; @@ -20,32 +21,50 @@ public class MetaDictionary : IEnumerable public IReadOnlyDictionary Imc => _imc; + public IReadOnlyDictionary Eqp + => _eqp; + + public IReadOnlyDictionary Eqdp + => _eqdp; + + public IReadOnlyDictionary Est + => _est; + + public IReadOnlyDictionary Gmp + => _gmp; + + public IReadOnlyDictionary Rsp + => _rsp; + + public IReadOnlySet GlobalEqp + => _globalEqp; + public int Count { get; private set; } - public int GetCount(MetaManipulation.Type type) + public int GetCount(MetaManipulationType type) => type switch { - MetaManipulation.Type.Imc => _imc.Count, - MetaManipulation.Type.Eqdp => _eqdp.Count, - MetaManipulation.Type.Eqp => _eqp.Count, - MetaManipulation.Type.Est => _est.Count, - MetaManipulation.Type.Gmp => _gmp.Count, - MetaManipulation.Type.Rsp => _rsp.Count, - MetaManipulation.Type.GlobalEqp => _globalEqp.Count, - _ => 0, + MetaManipulationType.Imc => _imc.Count, + MetaManipulationType.Eqdp => _eqdp.Count, + MetaManipulationType.Eqp => _eqp.Count, + MetaManipulationType.Est => _est.Count, + MetaManipulationType.Gmp => _gmp.Count, + MetaManipulationType.Rsp => _rsp.Count, + MetaManipulationType.GlobalEqp => _globalEqp.Count, + _ => 0, }; - public bool CanAdd(IMetaIdentifier identifier) + public bool Contains(IMetaIdentifier identifier) => identifier switch { - EqdpIdentifier eqdpIdentifier => !_eqdp.ContainsKey(eqdpIdentifier), - EqpIdentifier eqpIdentifier => !_eqp.ContainsKey(eqpIdentifier), - EstIdentifier estIdentifier => !_est.ContainsKey(estIdentifier), - GlobalEqpManipulation globalEqpManipulation => !_globalEqp.Contains(globalEqpManipulation), - GmpIdentifier gmpIdentifier => !_gmp.ContainsKey(gmpIdentifier), - ImcIdentifier imcIdentifier => !_imc.ContainsKey(imcIdentifier), - RspIdentifier rspIdentifier => !_rsp.ContainsKey(rspIdentifier), - _ => false, + EqdpIdentifier i => _eqdp.ContainsKey(i), + EqpIdentifier i => _eqp.ContainsKey(i), + EstIdentifier i => _est.ContainsKey(i), + GlobalEqpManipulation i => _globalEqp.Contains(i), + GmpIdentifier i => _gmp.ContainsKey(i), + ImcIdentifier i => _imc.ContainsKey(i), + RspIdentifier i => _rsp.ContainsKey(i), + _ => false, }; public void Clear() @@ -69,17 +88,16 @@ public class MetaDictionary : IEnumerable && _gmp.SetEquals(other._gmp) && _globalEqp.SetEquals(other._globalEqp); - public IEnumerator GetEnumerator() - => _imc.Select(kvp => new MetaManipulation(new ImcManipulation(kvp.Key, kvp.Value))) - .Concat(_eqp.Select(kvp => new MetaManipulation(new EqpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) - .Concat(_eqdp.Select(kvp => new MetaManipulation(new EqdpManipulation(kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))))) - .Concat(_est.Select(kvp => new MetaManipulation(new EstManipulation(kvp.Key, kvp.Value)))) - .Concat(_rsp.Select(kvp => new MetaManipulation(new RspManipulation(kvp.Key, kvp.Value)))) - .Concat(_gmp.Select(kvp => new MetaManipulation(new GmpManipulation(kvp.Key, kvp.Value)))) - .Concat(_globalEqp.Select(manip => new MetaManipulation(manip))).GetEnumerator(); + public IEnumerable Identifiers + => _imc.Keys.Cast() + .Concat(_eqdp.Keys.Cast()) + .Concat(_eqp.Keys.Cast()) + .Concat(_est.Keys.Cast()) + .Concat(_gmp.Keys.Cast()) + .Concat(_rsp.Keys.Cast()) + .Concat(_globalEqp.Cast()); - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); + #region TryAdd public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) { @@ -90,7 +108,6 @@ public class MetaDictionary : IEnumerable return true; } - public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) { if (!_eqp.TryAdd(identifier, entry)) @@ -103,7 +120,6 @@ public class MetaDictionary : IEnumerable public bool TryAdd(EqpIdentifier identifier, EqpEntry entry) => TryAdd(identifier, new EqpEntryInternal(entry, identifier.Slot)); - public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) { if (!_eqdp.TryAdd(identifier, entry)) @@ -152,6 +168,10 @@ public class MetaDictionary : IEnumerable return true; } + #endregion + + #region Update + public bool Update(ImcIdentifier identifier, ImcEntry entry) { if (!_imc.ContainsKey(identifier)) @@ -161,7 +181,6 @@ public class MetaDictionary : IEnumerable return true; } - public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) { if (!_eqp.ContainsKey(identifier)) @@ -174,7 +193,6 @@ public class MetaDictionary : IEnumerable public bool Update(EqpIdentifier identifier, EqpEntry entry) => Update(identifier, new EqpEntryInternal(entry, identifier.Slot)); - public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) { if (!_eqdp.ContainsKey(identifier)) @@ -214,6 +232,50 @@ public class MetaDictionary : IEnumerable return true; } + #endregion + + #region TryGetValue + + public bool TryGetValue(EstIdentifier identifier, out EstEntry value) + => _est.TryGetValue(identifier, out value); + + public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) + => _eqp.TryGetValue(identifier, out value); + + public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) + => _eqdp.TryGetValue(identifier, out value); + + public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) + => _gmp.TryGetValue(identifier, out value); + + public bool TryGetValue(RspIdentifier identifier, out RspEntry value) + => _rsp.TryGetValue(identifier, out value); + + public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) + => _imc.TryGetValue(identifier, out value); + + #endregion + + public bool Remove(IMetaIdentifier identifier) + { + var ret = identifier switch + { + EqdpIdentifier i => _eqdp.Remove(i), + EqpIdentifier i => _eqp.Remove(i), + EstIdentifier i => _est.Remove(i), + GlobalEqpManipulation i => _globalEqp.Remove(i), + GmpIdentifier i => _gmp.Remove(i), + ImcIdentifier i => _imc.Remove(i), + RspIdentifier i => _rsp.Remove(i), + _ => false, + }; + if (ret) + --Count; + return ret; + } + + #region Merging + public void UnionWith(MetaDictionary manips) { foreach (var (identifier, entry) in manips._imc) @@ -287,24 +349,6 @@ public class MetaDictionary : IEnumerable return false; } - public bool TryGetValue(EstIdentifier identifier, out EstEntry value) - => _est.TryGetValue(identifier, out value); - - public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) - => _eqp.TryGetValue(identifier, out value); - - public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) - => _eqdp.TryGetValue(identifier, out value); - - public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) - => _gmp.TryGetValue(identifier, out value); - - public bool TryGetValue(RspIdentifier identifier, out RspEntry value) - => _rsp.TryGetValue(identifier, out value); - - public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) - => _imc.TryGetValue(identifier, out value); - public void SetTo(MetaDictionary other) { _imc.SetTo(other._imc); @@ -329,6 +373,8 @@ public class MetaDictionary : IEnumerable Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; } + #endregion + public MetaDictionary Clone() { var ret = new MetaDictionary(); @@ -336,29 +382,124 @@ public class MetaDictionary : IEnumerable return ret; } - private static void WriteJson(JsonWriter writer, JsonSerializer serializer, IMetaIdentifier identifier, object entry) - { - var type = identifier switch + public static JObject Serialize(EqpIdentifier identifier, EqpEntryInternal entry) + => Serialize(identifier, entry.ToEntry(identifier.Slot)); + + public static JObject Serialize(EqpIdentifier identifier, EqpEntry entry) + => new() { - ImcIdentifier => "Imc", - EqdpIdentifier => "Eqdp", - EqpIdentifier => "Eqp", - EstIdentifier => "Est", - GmpIdentifier => "Gmp", - RspIdentifier => "Rsp", - GlobalEqpManipulation => "GlobalEqp", - _ => string.Empty, + ["Type"] = MetaManipulationType.Eqp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = (ulong)entry, + }), }; - if (type.Length == 0) - return; + public static JObject Serialize(EqdpIdentifier identifier, EqdpEntryInternal entry) + => Serialize(identifier, entry.ToEntry(identifier.Slot)); - writer.WriteStartObject(); - writer.WritePropertyName("Type"); - writer.WriteValue(type); - writer.WritePropertyName("Manipulation"); + public static JObject Serialize(EqdpIdentifier identifier, EqdpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Eqdp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = (ushort)entry, + }), + }; - writer.WriteEndObject(); + public static JObject Serialize(EstIdentifier identifier, EstEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Est.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + + public static JObject Serialize(GmpIdentifier identifier, GmpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Gmp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = JObject.FromObject(entry), + }), + }; + + public static JObject Serialize(ImcIdentifier identifier, ImcEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Imc.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = JObject.FromObject(entry), + }), + }; + + public static JObject Serialize(RspIdentifier identifier, RspEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Rsp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + + public static JObject Serialize(GlobalEqpManipulation identifier) + => new() + { + ["Type"] = MetaManipulationType.GlobalEqp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject()), + }; + + public static JObject? Serialize(TIdentifier identifier, TEntry entry) + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged + { + if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntryInternal)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqpIdentifier) && typeof(TEntry) == typeof(EqpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntryInternal)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EqdpIdentifier) && typeof(TEntry) == typeof(EqdpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(EstIdentifier) && typeof(TEntry) == typeof(EstEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(GmpIdentifier) && typeof(TEntry) == typeof(GmpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(RspIdentifier) && typeof(TEntry) == typeof(RspEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) + return Serialize(Unsafe.As(ref identifier)); + + return null; + } + + public static JArray SerializeTo(JArray array, IEnumerable> manipulations) + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged + { + foreach (var (identifier, entry) in manipulations) + { + if (Serialize(identifier, entry) is { } jObj) + array.Add(jObj); + } + + return array; + } + + public static JArray SerializeTo(JArray array, IEnumerable manipulations) + { + foreach (var manip in manipulations) + array.Add(Serialize(manip)); + + return array; } private class Converter : JsonConverter @@ -371,30 +512,27 @@ public class MetaDictionary : IEnumerable return; } - writer.WriteStartArray(); - foreach (var item in value) - { - writer.WriteStartObject(); - writer.WritePropertyName("Type"); - writer.WriteValue(item.ManipulationType.ToString()); - writer.WritePropertyName("Manipulation"); - serializer.Serialize(writer, item.Manipulation); - writer.WriteEndObject(); - } - - writer.WriteEndArray(); + var array = new JArray(); + SerializeTo(array, value._imc); + SerializeTo(array, value._eqp); + SerializeTo(array, value._eqdp); + SerializeTo(array, value._est); + SerializeTo(array, value._rsp); + SerializeTo(array, value._gmp); + SerializeTo(array, value._globalEqp); + array.WriteTo(writer); } public override MetaDictionary ReadJson(JsonReader reader, Type objectType, MetaDictionary? existingValue, bool hasExistingValue, JsonSerializer serializer) { - var dict = existingValue ?? []; + var dict = existingValue ?? new MetaDictionary(); dict.Clear(); var jObj = JArray.Load(reader); foreach (var item in jObj) { - var type = item["Type"]?.ToObject() ?? MetaManipulation.Type.Unknown; - if (type is MetaManipulation.Type.Unknown) + var type = item["Type"]?.ToObject() ?? MetaManipulationType.Unknown; + if (type is MetaManipulationType.Unknown) { Penumbra.Log.Warning($"Invalid Meta Manipulation Type {type} encountered."); continue; @@ -408,7 +546,7 @@ public class MetaDictionary : IEnumerable switch (type) { - case MetaManipulation.Type.Imc: + case MetaManipulationType.Imc: { var identifier = ImcIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -418,7 +556,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid IMC Manipulation encountered."); break; } - case MetaManipulation.Type.Eqdp: + case MetaManipulationType.Eqdp: { var identifier = EqdpIdentifier.FromJson(manip); var entry = (EqdpEntry?)manip["Entry"]?.ToObject(); @@ -428,7 +566,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid EQDP Manipulation encountered."); break; } - case MetaManipulation.Type.Eqp: + case MetaManipulationType.Eqp: { var identifier = EqpIdentifier.FromJson(manip); var entry = (EqpEntry?)manip["Entry"]?.ToObject(); @@ -438,7 +576,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid EQP Manipulation encountered."); break; } - case MetaManipulation.Type.Est: + case MetaManipulationType.Est: { var identifier = EstIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -448,7 +586,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid EST Manipulation encountered."); break; } - case MetaManipulation.Type.Gmp: + case MetaManipulationType.Gmp: { var identifier = GmpIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -458,7 +596,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid GMP Manipulation encountered."); break; } - case MetaManipulation.Type.Rsp: + case MetaManipulationType.Rsp: { var identifier = RspIdentifier.FromJson(manip); var entry = manip["Entry"]?.ToObject(); @@ -468,7 +606,7 @@ public class MetaDictionary : IEnumerable Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; } - case MetaManipulation.Type.GlobalEqp: + case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); if (identifier.HasValue) @@ -483,4 +621,22 @@ public class MetaDictionary : IEnumerable return dict; } } + + public MetaDictionary() + { } + + public MetaDictionary(MetaCache? cache) + { + if (cache == null) + return; + + _imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + _eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); + Count = cache.Count; + } } diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs deleted file mode 100644 index b80681d2..00000000 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ /dev/null @@ -1,457 +0,0 @@ -using Dalamud.Interface; -using ImGuiNET; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using OtterGui; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Mods.Editor; -using Penumbra.String.Functions; -using Penumbra.UI; -using Penumbra.UI.ModsTab; - -namespace Penumbra.Meta.Manipulations; - -#if false -private static class ImcRow -{ - private static ImcIdentifier _newIdentifier = ImcIdentifier.Default; - - private static float IdWidth - => 80 * UiHelpers.Scale; - - private static float SmallIdWidth - => 45 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, - editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var (defaultEntry, fileExists, _) = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true); - var manip = (MetaManipulation)new ImcManipulation(_newIdentifier, defaultEntry); - var canAdd = fileExists && editor.MetaEditor.CanAdd(manip); - var tt = canAdd ? "Stage this edit." : !fileExists ? "This IMC file does not exist." : "This entry is already edited."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(manip); - - // Identifier - ImGui.TableNextColumn(); - var change = ImcManipulationDrawer.DrawObjectType(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawPrimaryId(ref _newIdentifier); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - - ImGui.TableNextColumn(); - // Equipment and accessories are slightly different imcs than other types. - if (_newIdentifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier); - else - change |= ImcManipulationDrawer.DrawSecondaryId(ref _newIdentifier); - - ImGui.TableNextColumn(); - change |= ImcManipulationDrawer.DrawVariant(ref _newIdentifier); - - ImGui.TableNextColumn(); - if (_newIdentifier.ObjectType is ObjectType.DemiHuman) - change |= ImcManipulationDrawer.DrawSlot(ref _newIdentifier, 70); - else - ImUtf8.ScaledDummy(new Vector2(70 * UiHelpers.Scale, 0)); - - if (change) - defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(_newIdentifier, true).Entry; - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawDecalId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawVfxId(defaultEntry, ref defaultEntry, false); - ImGui.SameLine(); - ImcManipulationDrawer.DrawSoundId(defaultEntry, ref defaultEntry, false); - ImGui.TableNextColumn(); - ImcManipulationDrawer.DrawAttributes(defaultEntry, ref defaultEntry); - } - - public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.ObjectType.ToName()); - ImGuiUtil.HoverTooltip(ObjectTypeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - else - { - ImGui.TextUnformatted(meta.SecondaryId.ToString()); - ImGuiUtil.HoverTooltip(SecondaryIdTooltip); - } - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Variant.ToString()); - ImGuiUtil.HoverTooltip(VariantIdTooltip); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - if (meta.ObjectType is ObjectType.DemiHuman) - { - ImGui.TextUnformatted(meta.EquipSlot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - } - - // Values - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.TableNextColumn(); - var defaultEntry = metaFileManager.ImcChecker.GetDefaultEntry(meta.Identifier, true).Entry; - var newEntry = meta.Entry; - var changes = ImcManipulationDrawer.DrawMaterialId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawMaterialAnimationId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawDecalId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawVfxId(defaultEntry, ref newEntry, true); - ImGui.SameLine(); - changes |= ImcManipulationDrawer.DrawSoundId(defaultEntry, ref newEntry, true); - ImGui.TableNextColumn(); - changes |= ImcManipulationDrawer.DrawAttributes(defaultEntry, ref newEntry); - - if (changes) - editor.MetaEditor.Change(meta.Copy(newEntry)); - } -} - -#endif - -public interface IMetaManipulation -{ - public MetaIndex FileIndex(); -} - -public interface IMetaManipulation - : IMetaManipulation, IComparable, IEquatable where T : struct -{ } - -[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 16)] -public readonly struct MetaManipulation : IEquatable, IComparable -{ - public const int CurrentVersion = 0; - - public enum Type : byte - { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5, - Rsp = 6, - GlobalEqp = 7, - } - - [FieldOffset(0)] - [JsonIgnore] - public readonly EqpManipulation Eqp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly GmpManipulation Gmp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly EqdpManipulation Eqdp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly EstManipulation Est = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly RspManipulation Rsp = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly ImcManipulation Imc = default; - - [FieldOffset(0)] - [JsonIgnore] - public readonly GlobalEqpManipulation GlobalEqp = default; - - [FieldOffset(15)] - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty("Type")] - public readonly Type ManipulationType; - - public object? Manipulation - { - get => ManipulationType switch - { - Type.Unknown => null, - Type.Imc => Imc, - Type.Eqdp => Eqdp, - Type.Eqp => Eqp, - Type.Est => Est, - Type.Gmp => Gmp, - Type.Rsp => Rsp, - Type.GlobalEqp => GlobalEqp, - _ => null, - }; - init - { - switch (value) - { - case EqpManipulation m: - Eqp = m; - ManipulationType = m.Validate() ? Type.Eqp : Type.Unknown; - return; - case EqdpManipulation m: - Eqdp = m; - ManipulationType = m.Validate() ? Type.Eqdp : Type.Unknown; - return; - case GmpManipulation m: - Gmp = m; - ManipulationType = m.Validate() ? Type.Gmp : Type.Unknown; - return; - case EstManipulation m: - Est = m; - ManipulationType = m.Validate() ? Type.Est : Type.Unknown; - return; - case RspManipulation m: - Rsp = m; - ManipulationType = m.Validate() ? Type.Rsp : Type.Unknown; - return; - case ImcManipulation m: - Imc = m; - ManipulationType = m.Validate(true) ? Type.Imc : Type.Unknown; - return; - case GlobalEqpManipulation m: - GlobalEqp = m; - ManipulationType = m.Validate() ? Type.GlobalEqp : Type.Unknown; - return; - } - } - } - - public bool Validate() - { - return ManipulationType switch - { - Type.Imc => Imc.Validate(true), - Type.Eqdp => Eqdp.Validate(), - Type.Eqp => Eqp.Validate(), - Type.Est => Est.Validate(), - Type.Gmp => Gmp.Validate(), - Type.Rsp => Rsp.Validate(), - Type.GlobalEqp => GlobalEqp.Validate(), - _ => false, - }; - } - - public MetaManipulation(EqpManipulation eqp) - { - Eqp = eqp; - ManipulationType = Type.Eqp; - } - - public MetaManipulation(GmpManipulation gmp) - { - Gmp = gmp; - ManipulationType = Type.Gmp; - } - - public MetaManipulation(EqdpManipulation eqdp) - { - Eqdp = eqdp; - ManipulationType = Type.Eqdp; - } - - public MetaManipulation(EstManipulation est) - { - Est = est; - ManipulationType = Type.Est; - } - - public MetaManipulation(RspManipulation rsp) - { - Rsp = rsp; - ManipulationType = Type.Rsp; - } - - public MetaManipulation(ImcManipulation imc) - { - Imc = imc; - ManipulationType = Type.Imc; - } - - public MetaManipulation(GlobalEqpManipulation eqp) - { - GlobalEqp = eqp; - ManipulationType = Type.GlobalEqp; - } - - public static implicit operator MetaManipulation(EqpManipulation eqp) - => new(eqp); - - public static implicit operator MetaManipulation(GmpManipulation gmp) - => new(gmp); - - public static implicit operator MetaManipulation(EqdpManipulation eqdp) - => new(eqdp); - - public static implicit operator MetaManipulation(EstManipulation est) - => new(est); - - public static implicit operator MetaManipulation(RspManipulation rsp) - => new(rsp); - - public static implicit operator MetaManipulation(ImcManipulation imc) - => new(imc); - - public static implicit operator MetaManipulation(GlobalEqpManipulation eqp) - => new(eqp); - - public bool EntryEquals(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return false; - - return ManipulationType switch - { - Type.Eqp => Eqp.Entry.Equals(other.Eqp.Entry), - Type.Gmp => Gmp.Entry.Equals(other.Gmp.Entry), - Type.Eqdp => Eqdp.Entry.Equals(other.Eqdp.Entry), - Type.Est => Est.Entry.Equals(other.Est.Entry), - Type.Rsp => Rsp.Entry.Equals(other.Rsp.Entry), - Type.Imc => Imc.Entry.Equals(other.Imc.Entry), - Type.GlobalEqp => true, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public bool Equals(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return false; - - return ManipulationType switch - { - Type.Eqp => Eqp.Equals(other.Eqp), - Type.Gmp => Gmp.Equals(other.Gmp), - Type.Eqdp => Eqdp.Equals(other.Eqdp), - Type.Est => Est.Equals(other.Est), - Type.Rsp => Rsp.Equals(other.Rsp), - Type.Imc => Imc.Equals(other.Imc), - Type.GlobalEqp => GlobalEqp.Equals(other.GlobalEqp), - _ => false, - }; - } - - public MetaManipulation WithEntryOf(MetaManipulation other) - { - if (ManipulationType != other.ManipulationType) - return this; - - return ManipulationType switch - { - Type.Eqp => Eqp.Copy(other.Eqp.Entry), - Type.Gmp => Gmp.Copy(other.Gmp.Entry), - Type.Eqdp => Eqdp.Copy(other.Eqdp), - Type.Est => Est.Copy(other.Est.Entry), - Type.Rsp => Rsp.Copy(other.Rsp.Entry), - Type.Imc => Imc.Copy(other.Imc.Entry), - Type.GlobalEqp => GlobalEqp, - _ => throw new ArgumentOutOfRangeException(), - }; - } - - public override bool Equals(object? obj) - => obj is MetaManipulation other && Equals(other); - - public override int GetHashCode() - => ManipulationType switch - { - Type.Eqp => Eqp.GetHashCode(), - Type.Gmp => Gmp.GetHashCode(), - Type.Eqdp => Eqdp.GetHashCode(), - Type.Est => Est.GetHashCode(), - Type.Rsp => Rsp.GetHashCode(), - Type.Imc => Imc.GetHashCode(), - Type.GlobalEqp => GlobalEqp.GetHashCode(), - _ => 0, - }; - - public unsafe int CompareTo(MetaManipulation other) - { - fixed (MetaManipulation* lhs = &this) - { - return MemoryUtility.MemCmpUnchecked(lhs, &other, sizeof(MetaManipulation)); - } - } - - public override string ToString() - => ManipulationType switch - { - Type.Eqp => Eqp.ToString(), - Type.Gmp => Gmp.ToString(), - Type.Eqdp => Eqdp.ToString(), - Type.Est => Est.ToString(), - Type.Rsp => Rsp.ToString(), - Type.Imc => Imc.ToString(), - Type.GlobalEqp => GlobalEqp.ToString(), - _ => "Invalid", - }; - - public string EntryToString() - => ManipulationType switch - { - Type.Imc => - $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}", - Type.Eqdp => $"{(ushort)Eqdp.Entry:X}", - Type.Eqp => $"{(ulong)Eqp.Entry:X}", - Type.Est => $"{Est.Entry}", - Type.Gmp => $"{Gmp.Entry.Value}", - Type.Rsp => $"{Rsp.Entry}", - Type.GlobalEqp => string.Empty, - _ => string.Empty, - }; - - public static bool operator ==(MetaManipulation left, MetaManipulation right) - => left.Equals(right); - - public static bool operator !=(MetaManipulation left, MetaManipulation right) - => !(left == right); - - public static bool operator <(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) < 0; - - public static bool operator <=(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) <= 0; - - public static bool operator >(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) > 0; - - public static bool operator >=(MetaManipulation left, MetaManipulation right) - => left.CompareTo(right) >= 0; -} - diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index ca7cb1c5..73d1d7e5 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -38,6 +38,9 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib var ret = new RspIdentifier(subRace, attribute); return ret.Validate() ? ret : null; } + + public MetaManipulationType Type + => MetaManipulationType.Rsp; } [JsonConverter(typeof(Converter))] diff --git a/Penumbra/Meta/Manipulations/RspManipulation.cs b/Penumbra/Meta/Manipulations/RspManipulation.cs deleted file mode 100644 index e2282c41..00000000 --- a/Penumbra/Meta/Manipulations/RspManipulation.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Meta.Files; - -namespace Penumbra.Meta.Manipulations; - -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public readonly struct RspManipulation(RspIdentifier identifier, RspEntry entry) : IMetaManipulation -{ - [JsonIgnore] - public RspIdentifier Identifier { get; } = identifier; - - public RspEntry Entry { get; } = entry; - - [JsonConverter(typeof(StringEnumConverter))] - public SubRace SubRace - => Identifier.SubRace; - - [JsonConverter(typeof(StringEnumConverter))] - public RspAttribute Attribute - => Identifier.Attribute; - - [JsonConstructor] - public RspManipulation(SubRace subRace, RspAttribute attribute, RspEntry entry) - : this(new RspIdentifier(subRace, attribute), entry) - { } - - public RspManipulation Copy(RspEntry entry) - => new(Identifier, entry); - - public override string ToString() - => $"Rsp - {SubRace.ToName()} - {Attribute.ToFullString()}"; - - public bool Equals(RspManipulation other) - => SubRace == other.SubRace - && Attribute == other.Attribute; - - public override bool Equals(object? obj) - => obj is RspManipulation other && Equals(other); - - public override int GetHashCode() - => HashCode.Combine((int)SubRace, (int)Attribute); - - public int CompareTo(RspManipulation other) - { - var s = SubRace.CompareTo(other.SubRace); - return s != 0 ? s : Attribute.CompareTo(other.Attribute); - } - - public MetaIndex FileIndex() - => MetaIndex.HumanCmp; - - public bool Apply(CmpFile file) - { - var value = file[SubRace, Attribute]; - if (value == Entry) - return false; - - file[SubRace, Attribute] = Entry; - return true; - } - - public bool Validate() - => Identifier.Validate() && Entry.Validate(); -} diff --git a/Penumbra/Mods/Editor/IMod.cs b/Penumbra/Mods/Editor/IMod.cs index 06c31846..3da38829 100644 --- a/Penumbra/Mods/Editor/IMod.cs +++ b/Penumbra/Mods/Editor/IMod.cs @@ -10,7 +10,7 @@ public record struct AppliedModData( Dictionary FileRedirections, MetaDictionary Manipulations) { - public static readonly AppliedModData Empty = new([], []); + public static readonly AppliedModData Empty = new([], new MetaDictionary()); } public interface IMod diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 42171378..bacf4122 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -26,20 +26,20 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService } } - public readonly FrozenDictionary OtherData = - Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); + public readonly FrozenDictionary OtherData = + Enum.GetValues().ToFrozenDictionary(t => t, _ => new OtherOptionData()); - public bool Changes { get; private set; } + public bool Changes { get; set; } public new void Clear() { + Changes = Count > 0; base.Clear(); - Changes = true; } public void Load(Mod mod, IModDataContainer currentOption) { - foreach (var type in Enum.GetValues()) + foreach (var type in Enum.GetValues()) OtherData[type].Clear(); foreach (var option in mod.AllDataContainers) @@ -48,13 +48,13 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService continue; var name = option.GetFullName(); - OtherData[MetaManipulation.Type.Imc].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Imc)); - OtherData[MetaManipulation.Type.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqp)); - OtherData[MetaManipulation.Type.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Eqdp)); - OtherData[MetaManipulation.Type.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Gmp)); - OtherData[MetaManipulation.Type.Est].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Est)); - OtherData[MetaManipulation.Type.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.Rsp)); - OtherData[MetaManipulation.Type.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulation.Type.GlobalEqp)); + OtherData[MetaManipulationType.Imc].Add(name, option.Manipulations.GetCount(MetaManipulationType.Imc)); + OtherData[MetaManipulationType.Eqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqp)); + OtherData[MetaManipulationType.Eqdp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Eqdp)); + OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp)); + OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); + OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); + OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } Clear(); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index e42a1d31..b7827c47 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -125,8 +125,8 @@ public static class EquipmentSwap _ => (EstType)0, }; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) @@ -242,8 +242,8 @@ public static class EquipmentSwap private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var entry = new ImcManipulation(slotFrom, variantFrom.Id, idFrom, default); - var imc = new ImcFile(manager, entry.Identifier); + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); EquipItem[] items; Variant[] variants; if (idFrom == idTo) @@ -273,7 +273,8 @@ public static class EquipmentSwap var manipToIdentifier = new GmpIdentifier(idTo); var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); - return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); } public static MetaSwap CreateImc(MetaFileManager manager, Func redirections, @@ -286,16 +287,17 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); - var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); var decal = CreateDecal(manager, redirections, imc.SwapToModdedEntry.DecalId); if (decal != null) imc.ChildSwaps.Add(decal); - var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModdedEntry.VfxId); + var avfx = CreateAvfx(manager, redirections, slotFrom, slotTo, idFrom, idTo, imc.SwapToModdedEntry.VfxId); if (avfx != null) imc.ChildSwaps.Add(avfx); @@ -316,19 +318,21 @@ public static class EquipmentSwap // Example: Abyssos Helm / Body - public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, PrimaryId idFrom, PrimaryId idTo, + public static FileSwap? CreateAvfx(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + PrimaryId idFrom, PrimaryId idTo, byte vfxId) { if (vfxId == 0) return null; var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); - var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); + var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { - var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged); + var atex = CreateAtex(manager, redirections, slotFrom, slotTo, idFrom, ref filePath, ref avfx.DataWasChanged); avfx.ChildSwaps.Add(atex); } @@ -394,8 +398,7 @@ public static class EquipmentSwap } public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, PrimaryId idFrom, - PrimaryId idTo, - ref MtrlFile.Texture texture, ref bool dataWasChanged) + PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) => CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged); public static FileSwap CreateTex(MetaFileManager manager, Func redirections, char prefix, EquipSlot slotFrom, @@ -404,6 +407,7 @@ public static class EquipmentSwap var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); + newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { @@ -421,11 +425,12 @@ public static class EquipmentSwap return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } - public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, ref string filePath, - ref bool dataWasChanged) + public static FileSwap CreateAtex(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, + PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index efd8080c..1f4c5e7a 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -136,14 +136,14 @@ public static class ItemSwap public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, EstManipulation.ToName(type), estEntry.AsId); + var phybPath = GamePaths.Skeleton.Phyb.Path(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, EstManipulation.ToName(type), estEntry.AsId); + var sklbPath = GamePaths.Skeleton.Sklb.Path(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } @@ -154,10 +154,11 @@ public static class ItemSwap return null; var manipFromIdentifier = new EstIdentifier(idFrom, type, genderRace); - var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); - var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); - var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + var manipToIdentifier = new EstIdentifier(idTo, type, genderRace); + var manipFromDefault = EstFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = EstFile.GetDefault(manager, manipToIdentifier); + var est = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, + manipToIdentifier, manipToDefault); if (ownMdl && est.SwapToModdedEntry.Value >= 2) { @@ -215,6 +216,22 @@ public static class ItemSwap ? path.Replace($"_{from.ToSuffix()}_", $"_{to.ToSuffix()}_") : path; + public static string ReplaceType(string path, EquipSlot from, EquipSlot to, PrimaryId idFrom) + { + var isAccessoryFrom = from.IsAccessory(); + if (isAccessoryFrom == to.IsAccessory()) + return path; + + if (isAccessoryFrom) + { + path = path.Replace("accessory/a", "equipment/e"); + return path.Replace($"a{idFrom.Id:D4}", $"e{idFrom.Id:D4}"); + } + + path = path.Replace("equipment/e", "accessory/a"); + return path.Replace($"e{idFrom.Id:D4}", $"a{idFrom.Id:D4}"); + } + public static string ReplaceRace(string path, GenderRace from, GenderRace to, bool condition = true) => ReplaceId(path, 'c', (ushort)from, (ushort)to, condition); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 8328edea..d2deb9ef 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -123,8 +123,8 @@ public class ItemSwapContainer : p => ModRedirections.TryGetValue(p, out var path) ? path : new FullPath(p); private MetaDictionary MetaResolver(ModCollection? collection) - => collection?.MetaCache?.Manipulations is { } cache - ? [] // [.. cache] TODO + => collection?.MetaCache is { } cache + ? new MetaDictionary(cache) : _appliedModData.Manipulations; public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index e8ca3199..ed4245c4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -198,8 +198,7 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - // TODO - option.Manipulations.UnionWith([]);//[.. meta.MetaManipulations]); + option.Manipulations.UnionWith(meta.MetaManipulations); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { @@ -213,8 +212,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - // TODO - option.Manipulations.UnionWith([]);//[.. rgsp.MetaManipulations]); + option.Manipulations.UnionWith(rgsp.MetaManipulations); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/DefaultSubMod.cs b/Penumbra/Mods/SubMods/DefaultSubMod.cs index dcd33610..3840468f 100644 --- a/Penumbra/Mods/SubMods/DefaultSubMod.cs +++ b/Penumbra/Mods/SubMods/DefaultSubMod.cs @@ -13,7 +13,7 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public MetaDictionary Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); IMod IModDataContainer.Mod => Mod; diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index ed7b6ff8..8fac52d8 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -33,7 +33,7 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; - public MetaDictionary Manipulations { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) => SubMod.AddContainerTo(this, redirections, manipulations); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 91c4c5df..e1cf9f2b 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -93,8 +93,7 @@ public class TemporaryMod : IMod } } - // TODO - MetaDictionary manips = []; // [.. collection.MetaCache?.Manipulations ?? []]; + var manips = new MetaDictionary(collection.MetaCache); defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs new file mode 100644 index 00000000..b1ac93f1 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -0,0 +1,159 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Racial Model Edits (EQDP)###EQDP"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new EqdpIdentifier(1, EquipSlot.Head, GenderRace.MidlanderMale); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, Identifier), Identifier.Slot); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp)); + + ImGui.TableNextColumn(); + var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; + var canAdd = validRaceCode && !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : + validRaceCode ? "This entry is already edited."u8 : "This combination of race and gender can not be used."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntryInternal entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(MetaFiles, identifier), identifier.Slot); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() + => Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawEquipSlot(ref identifier); + return changes; + } + + private static void DrawIdentifier(EqdpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Gender"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(EqdpEntryInternal defaultEntry, ref EqdpEntryInternal entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + if (Checkmark("Material##eqdp"u8, "\0"u8, entry.Material, defaultEntry.Material, out var newMaterial)) + { + entry = entry with { Material = newMaterial }; + changes = true; + } + + ImGui.SameLine(); + if (Checkmark("Model##eqdp"u8, "\0"u8, entry.Model, defaultEntry.Model, out var newModel)) + { + entry = entry with { Material = newModel }; + changes = true; + } + + return changes; + } + + public static bool DrawPrimaryId(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##eqdpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawRace(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##eqdpRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + return ret; + } + + public static bool DrawGender(ref EqdpIdentifier identifier, float unscaledWidth = 120) + { + var ret = Combos.Gender("##eqdpGender", identifier.Gender, out var gender, unscaledWidth); + ImUtf8.HoverTooltip("Gender"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) }; + return ret; + } + + public static bool DrawEquipSlot(ref EqdpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.EqdpEquipSlot("##eqdpSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs new file mode 100644 index 00000000..56c06bc9 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -0,0 +1,134 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Equipment Parameter Edits (EQP)###EQP"u8; + + public override int NumColumns + => 5; + + protected override void Initialize() + { + Identifier = new EqpIdentifier(1, EquipSlot.Body); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, Identifier.SetId), Identifier.Slot); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Identifier.Slot, Entry, ref Entry, true); + } + + protected override void DrawEntry(EqpIdentifier identifier, EqpEntryInternal entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(MetaFiles, identifier.SetId), identifier.Slot); + if (DrawEntry(identifier.Slot, defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() + => Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EqpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawEquipSlot(ref identifier); + return changes; + } + + private static void DrawIdentifier(EqpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(EquipSlot slot, EqpEntryInternal defaultEntry, ref EqpEntryInternal entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var offset = Eqp.OffsetAndMask(slot).Item1; + DrawBox(ref entry, 0); + for (var i = 1; i < Eqp.EqpAttributes[slot].Count; ++i) + { + ImUtf8.SameLineInner(); + DrawBox(ref entry, i); + } + + return changes; + + void DrawBox(ref EqpEntryInternal entry, int i) + { + using var id = ImUtf8.PushId(i); + var flag = 1u << i; + var eqpFlag = (EqpEntry)((ulong)flag << offset); + var defaultValue = (flag & defaultEntry.Value) != 0; + var value = (flag & entry.Value) != 0; + if (Checkmark("##eqp"u8, eqpFlag.ToLocalName(), value, defaultValue, out var newValue)) + { + entry = new EqpEntryInternal(newValue ? entry.Value | flag : entry.Value & ~flag); + changes = true; + } + } + } + + public static bool DrawPrimaryId(ref EqpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##eqpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawEquipSlot(ref EqpIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.EqpEquipSlot("##eqpSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Equip Slot"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs new file mode 100644 index 00000000..5c3c5df5 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -0,0 +1,147 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Extra Skeleton Parameters (EST)###EST"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new EstIdentifier(1, EstType.Hair, GenderRace.MidlanderMale); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = EstFile.GetDefault(MetaFiles, Identifier.Slot, Identifier.GenderRace, Identifier.SetId); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = EstFile.GetDefault(MetaFiles, identifier.Slot, identifier.GenderRace, identifier.SetId); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() + => Editor.Est.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref EstIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawSlot(ref identifier); + + return changes; + } + + private static void DrawIdentifier(EstIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.HoverTooltip("Gender"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Slot.ToString(), FrameColor); + ImUtf8.HoverTooltip("Extra Skeleton Type"u8); + } + + private static bool DrawEntry(EstEntry defaultEntry, ref EstEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var ret = DragInput("##estValue"u8, [], 100f * ImUtf8.GlobalScale, entry.Value, defaultEntry.Value, out var newValue, (ushort)0, + ushort.MaxValue, 0.05f, !disabled); + if (ret) + entry = new EstEntry(newValue); + return ret; + } + + public static bool DrawPrimaryId(ref EstIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##estPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = identifier with { SetId = setId }; + return ret; + } + + public static bool DrawRace(ref EstIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##estRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + return ret; + } + + public static bool DrawGender(ref EstIdentifier identifier, float unscaledWidth = 120) + { + var ret = Combos.Gender("##estGender", identifier.Gender, out var gender, unscaledWidth); + ImUtf8.HoverTooltip("Gender"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(gender, identifier.Race) }; + return ret; + } + + public static bool DrawSlot(ref EstIdentifier identifier, float unscaledWidth = 200) + { + var ret = Combos.EstSlot("##estSlot", identifier.Slot, out var slot, unscaledWidth); + ImUtf8.HoverTooltip("Extra Skeleton Type"u8); + if (ret) + identifier = identifier with { Slot = slot }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs new file mode 100644 index 00000000..130831a0 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -0,0 +1,111 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8; + + public override int NumColumns + => 4; + + protected override void Initialize() + { + Identifier = new GlobalEqpManipulation() + { + Condition = 1, + Type = GlobalEqpType.DoNotHideEarrings, + }; + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier); + + DrawIdentifierInput(ref Identifier); + } + + protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) + { + DrawMetaButtons(identifier, 0); + DrawIdentifier(identifier); + } + + protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() + => Editor.GlobalEqp.Select(identifier => (identifier, (byte)0)); + + private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) + { + ImGui.TableNextColumn(); + DrawType(ref identifier); + + ImGui.TableNextColumn(); + if (identifier.Type.HasCondition()) + DrawCondition(ref identifier); + else + ImUtf8.ScaledDummy(100); + } + + private static void DrawIdentifier(GlobalEqpManipulation identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); + ImUtf8.HoverTooltip("Global EQP Type"u8); + + ImGui.TableNextColumn(); + if (identifier.Type.HasCondition()) + { + ImUtf8.TextFramed($"{identifier.Condition.Id}", FrameColor); + ImUtf8.HoverTooltip("Conditional Model ID"u8); + } + } + + public static bool DrawType(ref GlobalEqpManipulation identifier, float unscaledWidth = 250) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var combo = ImUtf8.Combo("##geqpType"u8, identifier.Type.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var type in Enum.GetValues()) + { + if (ImUtf8.Selectable(type.ToName(), type == identifier.Type)) + { + identifier = new GlobalEqpManipulation + { + Type = type, + Condition = type.HasCondition() ? identifier.Type.HasCondition() ? identifier.Condition : 1 : 0, + }; + ret = true; + } + + ImUtf8.HoverTooltip(type.ToDescription()); + } + + return ret; + } + + public static void DrawCondition(ref GlobalEqpManipulation identifier, float unscaledWidth = 100) + { + if (IdInput("##geqpCond"u8, unscaledWidth, identifier.Condition.Id, out var newId, 1, ushort.MaxValue, + identifier.Condition.Id <= 1)) + identifier = identifier with { Condition = newId }; + ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs new file mode 100644 index 00000000..87ed21dc --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -0,0 +1,148 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Files; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Visor/Gimmick Edits (GMP)###GMP"u8; + + public override int NumColumns + => 7; + + protected override void Initialize() + { + Identifier = new GmpIdentifier(1); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = ExpandedGmpFile.GetDefault(MetaFiles, Identifier.SetId); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = ExpandedGmpFile.GetDefault(MetaFiles, identifier.SetId); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() + => Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref GmpIdentifier identifier) + { + ImGui.TableNextColumn(); + return DrawPrimaryId(ref identifier); + } + + private static void DrawIdentifier(GmpIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.SetId.Id}", FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + } + + private static bool DrawEntry(GmpEntry defaultEntry, ref GmpEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var changes = false; + if (Checkmark("##gmpEnabled"u8, "Gimmick Enabled", entry.Enabled, defaultEntry.Enabled, out var enabled)) + { + entry = entry with { Enabled = enabled }; + changes = true; + } + + ImGui.TableNextColumn(); + if (Checkmark("##gmpAnimated"u8, "Gimmick Animated", entry.Animated, defaultEntry.Animated, out var animated)) + { + entry = entry with { Animated = animated }; + changes = true; + } + + var rotationWidth = 75 * ImUtf8.GlobalScale; + ImGui.TableNextColumn(); + if (DragInput("##gmpRotationA"u8, "Rotation A in Degrees"u8, rotationWidth, entry.RotationA, defaultEntry.RotationA, out var rotationA, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationA = rotationA }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpRotationB"u8, "Rotation B in Degrees"u8, rotationWidth, entry.RotationB, defaultEntry.RotationB, out var rotationB, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationB = rotationB }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpRotationC"u8, "Rotation C in Degrees"u8, rotationWidth, entry.RotationC, defaultEntry.RotationC, out var rotationC, + (ushort)0, (ushort)360, 0.05f, !disabled)) + { + entry = entry with { RotationC = rotationC }; + changes = true; + } + + var unkWidth = 50 * ImUtf8.GlobalScale; + ImGui.TableNextColumn(); + if (DragInput("##gmpUnkA"u8, "Animation Type A?"u8, unkWidth, entry.UnknownA, defaultEntry.UnknownA, out var unknownA, + (byte)0, (byte)15, 0.01f, !disabled)) + { + entry = entry with { UnknownA = unknownA }; + changes = true; + } + + ImUtf8.SameLineInner(); + if (DragInput("##gmpUnkB"u8, "Animation Type B?"u8, unkWidth, entry.UnknownB, defaultEntry.UnknownB, out var unknownB, + (byte)0, (byte)15, 0.01f, !disabled)) + { + entry = entry with { UnknownB = unknownB }; + changes = true; + } + + return changes; + } + + public static bool DrawPrimaryId(ref GmpIdentifier identifier, float unscaledWidth = 100) + { + var ret = IdInput("##gmpPrimaryId"u8, unscaledWidth, identifier.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, + identifier.SetId.Id <= 1); + ImUtf8.HoverTooltip( + "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."u8); + if (ret) + identifier = new GmpIdentifier(setId); + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index d9a8c27c..58f626fc 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -12,22 +12,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow.Meta; -public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) +public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : MetaDrawer(editor, metaFiles), IService { - private bool _fileExists; + public override ReadOnlySpan Label + => "Variant Edits (IMC)###IMC"u8; - private const string ModelSetIdTooltipShort = "Model Set ID"; - private const string EquipSlotTooltip = "Equip Slot"; - private const string ModelRaceTooltip = "Model Race"; - private const string GenderTooltip = "Gender"; - private const string ObjectTypeTooltip = "Object Type"; - private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIdTooltipShort = "Primary ID"; - private const string VariantIdTooltip = "Variant ID"; - private const string EstTypeTooltip = "EST Type"; - private const string RacialTribeTooltip = "Racial Tribe"; - private const string ScalingTypeTooltip = "Scaling Type"; + public override int NumColumns + => 10; + + private bool _fileExists; protected override void Initialize() { @@ -41,14 +35,14 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) protected override void DrawNew() { ImGui.TableNextColumn(); - // Copy To Clipboard + CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc)); ImGui.TableNextColumn(); - var canAdd = _fileExists && Editor.MetaEditor.CanAdd(Identifier); + var canAdd = _fileExists && !Editor.Contains(Identifier); var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) - Editor.MetaEditor.TryAdd(Identifier, Entry); + Editor.Changes |= Editor.TryAdd(Identifier, Entry); - if (DrawIdentifier(ref Identifier)) + if (DrawIdentifierInput(ref Identifier)) UpdateEntry(); using var disabled = ImRaii.Disabled(); @@ -57,46 +51,15 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) protected override void DrawEntry(ImcIdentifier identifier, ImcEntry entry) { - const uint frameColor = 0; - // Meta Buttons - - ImGui.TableNextColumn(); - ImUtf8.TextFramed(identifier.ObjectType.ToName(), frameColor); - ImUtf8.HoverTooltip("Object Type"u8); - - ImGui.TableNextColumn(); - ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", frameColor); - ImUtf8.HoverTooltip("Primary ID"); - - ImGui.TableNextColumn(); - if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) - { - ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); - ImUtf8.HoverTooltip("Equip Slot"u8); - } - else - { - ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", frameColor); - ImUtf8.HoverTooltip("Secondary ID"u8); - } - - ImGui.TableNextColumn(); - ImUtf8.TextFramed($"{identifier.Variant.Id}", frameColor); - ImUtf8.HoverTooltip("Variant"u8); - - ImGui.TableNextColumn(); - if (identifier.ObjectType is ObjectType.DemiHuman) - { - ImUtf8.TextFramed(identifier.EquipSlot.ToName(), frameColor); - ImUtf8.HoverTooltip("Equip Slot"u8); - } + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; if (DrawEntry(defaultEntry, ref entry, true)) - Editor.MetaEditor.Update(identifier, entry); + Editor.Changes |= Editor.Update(identifier, entry); } - private static bool DrawIdentifier(ref ImcIdentifier identifier) + private static bool DrawIdentifierInput(ref ImcIdentifier identifier) { ImGui.TableNextColumn(); var change = DrawObjectType(ref identifier); @@ -121,6 +84,41 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) return change; } + private static void DrawIdentifier(ImcIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.ObjectType.ToName(), FrameColor); + ImUtf8.HoverTooltip("Object Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.PrimaryId.Id}", FrameColor); + ImUtf8.HoverTooltip("Primary ID"); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.Equipment or ObjectType.Accessory) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + else + { + ImUtf8.TextFramed($"{identifier.SecondaryId.Id}", FrameColor); + ImUtf8.HoverTooltip("Secondary ID"u8); + } + + ImGui.TableNextColumn(); + ImUtf8.TextFramed($"{identifier.Variant.Id}", FrameColor); + ImUtf8.HoverTooltip("Variant"u8); + + ImGui.TableNextColumn(); + if (identifier.ObjectType is ObjectType.DemiHuman) + { + ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + } + private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) { ImGui.TableNextColumn(); @@ -142,7 +140,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() - => Editor.MetaEditor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Imc.Select(kvp => (kvp.Key, kvp.Value)); public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { @@ -270,7 +268,7 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) return true; } - public static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) + private static bool DrawAttributes(ImcEntry defaultEntry, ref ImcEntry entry) { var changes = false; for (var i = 0; i < ImcEntry.NumAttributes; ++i) @@ -292,62 +290,4 @@ public sealed class ImcMetaDrawer(ModEditor editor, MetaFileManager metaFiles) return changes; } - - - /// - /// A number input for ids with an optional max id of given width. - /// Returns true if newId changed against currentId. - /// - private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, - bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImUtf8.InputScalar(label, ref tmp)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - /// - /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - /// Returns true if newValue changed against currentValue. - /// - private static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, - out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) - newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; - - if (addDefault) - ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); - else - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - - return newValue != currentValue; - } - - /// - /// A checkmark that compares against a default value and shows a tooltip. - /// Returns true if newValue is changed against currentValue. - /// - private static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, - out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImUtf8.Checkbox(label, ref newValue); - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); - return newValue != currentValue; - } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs new file mode 100644 index 00000000..229526c4 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -0,0 +1,154 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.Api.Api; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public interface IMetaDrawer +{ + public ReadOnlySpan Label { get; } + public int NumColumns { get; } + public void Draw(); +} + +public abstract class MetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : IMetaDrawer + where TIdentifier : unmanaged, IMetaIdentifier + where TEntry : unmanaged +{ + protected const uint FrameColor = 0; + + protected readonly ModMetaEditor Editor = editor; + protected readonly MetaFileManager MetaFiles = metaFiles; + protected TIdentifier Identifier; + protected TEntry Entry; + private bool _initialized; + + public void Draw() + { + if (!_initialized) + { + Initialize(); + _initialized = true; + } + + using var id = ImUtf8.PushId((int)Identifier.Type); + DrawNew(); + foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) + { + id.Push(idx); + DrawEntry(identifier, entry); + id.Pop(); + } + } + + public abstract ReadOnlySpan Label { get; } + public abstract int NumColumns { get; } + + protected abstract void DrawNew(); + protected abstract void Initialize(); + protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); + + protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); + + + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// + protected static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, + bool border) + { + int tmp = currentId; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); + if (ImUtf8.InputScalar(label, ref tmp)) + tmp = Math.Clamp(tmp, minId, maxId); + + newId = (ushort)tmp; + return newId != currentId; + } + + /// + /// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. + /// Returns true if newValue changed against currentValue. + /// + protected static bool DragInput(ReadOnlySpan label, ReadOnlySpan tooltip, float width, T currentValue, T defaultValue, + out T newValue, T minValue, T maxValue, float speed, bool addDefault) where T : unmanaged, INumber + { + newValue = currentValue; + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + ImGui.SetNextItemWidth(width); + if (ImUtf8.DragScalar(label, ref newValue, minValue, maxValue, speed)) + newValue = newValue <= minValue ? minValue : newValue >= maxValue ? maxValue : newValue; + + if (addDefault) + ImUtf8.HoverTooltip($"{tooltip}\nDefault Value: {defaultValue}"); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + protected static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } + + /// + /// A checkmark that compares against a default value and shows a tooltip. + /// Returns true if newValue is changed against currentValue. + /// + protected static bool Checkmark(ReadOnlySpan label, ReadOnlySpan tooltip, bool currentValue, bool defaultValue, + out bool newValue) + { + using var color = ImRaii.PushColor(ImGuiCol.FrameBg, + defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue != currentValue); + newValue = currentValue; + ImUtf8.Checkbox(label, ref newValue); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tooltip); + return newValue != currentValue; + } + + protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); + + ImGui.TableNextColumn(); + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8)) + Editor.Changes |= Editor.Remove(identifier); + } + + protected void CopyToClipboardButton(ReadOnlySpan tooltip, JToken? manipulations) + { + if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) + return; + + var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + if (text.Length > 0) + ImGui.SetClipboardText(text); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs new file mode 100644 index 00000000..b3dd9299 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -0,0 +1,35 @@ +using OtterGui.Services; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public class MetaDrawers( + EqdpMetaDrawer eqdp, + EqpMetaDrawer eqp, + EstMetaDrawer est, + GlobalEqpMetaDrawer globalEqp, + GmpMetaDrawer gmp, + ImcMetaDrawer imc, + RspMetaDrawer rsp) : IService +{ + public readonly EqdpMetaDrawer Eqdp = eqdp; + public readonly EqpMetaDrawer Eqp = eqp; + public readonly EstMetaDrawer Est = est; + public readonly GmpMetaDrawer Gmp = gmp; + public readonly RspMetaDrawer Rsp = rsp; + public readonly ImcMetaDrawer Imc = imc; + public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; + + public IMetaDrawer? Get(MetaManipulationType type) + => type switch + { + MetaManipulationType.Imc => Imc, + MetaManipulationType.Eqdp => Eqdp, + MetaManipulationType.Eqp => Eqp, + MetaManipulationType.Est => Est, + MetaManipulationType.Gmp => Gmp, + MetaManipulationType.Rsp => Rsp, + MetaManipulationType.GlobalEqp => GlobalEqp, + _ => null, + }; +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs new file mode 100644 index 00000000..2b7904ce --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -0,0 +1,112 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Racial Scaling Edits (RSP)###RSP"u8; + + public override int NumColumns + => 5; + + protected override void Initialize() + { + Identifier = new RspIdentifier(SubRace.Midlander, RspAttribute.MaleMinSize); + UpdateEntry(); + } + + private void UpdateEntry() + => Entry = CmpFile.GetDefault(MetaFiles, Identifier.SubRace, Identifier.Attribute); + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp)); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + DrawEntry(Entry, ref Entry, true); + } + + protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = CmpFile.GetDefault(MetaFiles, identifier.SubRace, identifier.Attribute); + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() + => Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value)); + + private static bool DrawIdentifierInput(ref RspIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawSubRace(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawAttribute(ref identifier); + return changes; + } + + private static void DrawIdentifier(RspIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.SubRace.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Set ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Attribute.ToFullString(), FrameColor); + ImUtf8.HoverTooltip("Equip Slot"u8); + } + + private static bool DrawEntry(RspEntry defaultEntry, ref RspEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, defaultEntry.Value, entry.Value, out var newValue, + RspEntry.MinValue, RspEntry.MaxValue, 0.001f, !disabled); + if (ret) + entry = new RspEntry(newValue); + return ret; + } + + public static bool DrawSubRace(ref RspIdentifier identifier, float unscaledWidth = 150) + { + var ret = Combos.SubRace("##rspSubRace", identifier.SubRace, out var subRace, unscaledWidth); + ImUtf8.HoverTooltip("Racial Clan"u8); + if (ret) + identifier = identifier with { SubRace = subRace }; + return ret; + } + + public static bool DrawAttribute(ref RspIdentifier identifier, float unscaledWidth = 200) + { + var ret = Combos.RspAttribute("##rspAttribute", identifier.Attribute, out var attribute, unscaledWidth); + ImUtf8.HoverTooltip("Scaling Attribute"u8); + if (ret) + identifier = identifier with { Attribute = attribute }; + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 50862eec..3ec6a4d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -1,27 +1,18 @@ -using System.Reflection.Emit; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using OtterGui.Text.EndObjects; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.Meta; -using Penumbra.Meta.Files; +using Penumbra.Api.Api; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const string ModelSetIdTooltip = - "Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that."; - - + private readonly MetaDrawers _metaDrawers; private void DrawMetaTab() { @@ -56,80 +47,42 @@ public partial class ModEditWindow if (!child) return; - DrawEditHeader(MetaManipulation.Type.Eqp); - DrawEditHeader(MetaManipulation.Type.Eqdp); - DrawEditHeader(MetaManipulation.Type.Imc); - DrawEditHeader(MetaManipulation.Type.Est); - DrawEditHeader(MetaManipulation.Type.Gmp); - DrawEditHeader(MetaManipulation.Type.Rsp); - DrawEditHeader(MetaManipulation.Type.GlobalEqp); + DrawEditHeader(MetaManipulationType.Eqp); + DrawEditHeader(MetaManipulationType.Eqdp); + DrawEditHeader(MetaManipulationType.Imc); + DrawEditHeader(MetaManipulationType.Est); + DrawEditHeader(MetaManipulationType.Gmp); + DrawEditHeader(MetaManipulationType.Rsp); + DrawEditHeader(MetaManipulationType.GlobalEqp); } - private static ReadOnlySpan Label(MetaManipulation.Type type) - => type switch - { - MetaManipulation.Type.Imc => "Variant Edits (IMC)###IMC"u8, - MetaManipulation.Type.Eqdp => "Racial Model Edits (EQDP)###EQDP"u8, - MetaManipulation.Type.Eqp => "Equipment Parameter Edits (EQP)###EQP"u8, - MetaManipulation.Type.Est => "Extra Skeleton Parameters (EST)###EST"u8, - MetaManipulation.Type.Gmp => "Visor/Gimmick Edits (GMP)###GMP"u8, - MetaManipulation.Type.Rsp => "Racial Scaling Edits (RSP)###RSP"u8, - MetaManipulation.Type.GlobalEqp => "Global Equipment Parameter Edits (Global EQP)###GEQP"u8, - _ => "\0"u8, - }; - - private static int ColumnCount(MetaManipulation.Type type) - => type switch - { - MetaManipulation.Type.Imc => 10, - MetaManipulation.Type.Eqdp => 7, - MetaManipulation.Type.Eqp => 5, - MetaManipulation.Type.Est => 7, - MetaManipulation.Type.Gmp => 7, - MetaManipulation.Type.Rsp => 5, - MetaManipulation.Type.GlobalEqp => 4, - _ => 0, - }; - - private void DrawEditHeader(MetaManipulation.Type type) + private void DrawEditHeader(MetaManipulationType type) { + var drawer = _metaDrawers.Get(type); + if (drawer == null) + return; + var oldPos = ImGui.GetCursorPosY(); - var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {Label(type)}"); + var header = ImUtf8.CollapsingHeader($"{_editor.MetaEditor.GetCount(type)} {drawer.Label}"); DrawOtherOptionData(type, oldPos, ImGui.GetCursorPos()); if (!header) return; - DrawTable(type); + DrawTable(drawer); } - private IMetaDrawer? Drawer(MetaManipulation.Type type) - => type switch - { - //MetaManipulation.Type.Imc => expr, - //MetaManipulation.Type.Eqdp => expr, - //MetaManipulation.Type.Eqp => expr, - //MetaManipulation.Type.Est => expr, - //MetaManipulation.Type.Gmp => expr, - //MetaManipulation.Type.Rsp => expr, - //MetaManipulation.Type.GlobalEqp => expr, - _ => null, - }; - - private void DrawTable(MetaManipulation.Type type) + private static void DrawTable(IMetaDrawer drawer) { const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; - using var table = ImUtf8.Table(Label(type), ColumnCount(type), flags); + using var table = ImUtf8.Table(drawer.Label, drawer.NumColumns, flags); if (!table) return; - if (Drawer(type) is not { } drawer) - return; - drawer.Draw(); ImGui.NewLine(); } - private void DrawOtherOptionData(MetaManipulation.Type type, float oldPos, Vector2 newPos) + private void DrawOtherOptionData(MetaManipulationType type, float oldPos, Vector2 newPos) { var otherOptionData = _editor.MetaEditor.OtherData[type]; if (otherOptionData.TotalCount <= 0) @@ -149,577 +102,12 @@ public partial class ModEditWindow ImGui.SetCursorPos(newPos); } -#if false - private static class EqpRow - { - private static EqpIdentifier _newIdentifier = new(1, EquipSlot.Body); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, - editor.MetaEditor.Eqp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##eqpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId); - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.EqpEquipSlot("##eqpSlot", _new.Slot, out var slot)) - _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId); - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - foreach (var flag in Eqp.EqpAttributes[_new.Slot]) - { - var value = defaultEntry.HasFlag(flag); - Checkmark("##eqp", flag.ToLocalName(), value, value, out _); - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - - public static void Draw(MetaFileManager metaFileManager, EqpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId); - - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - ImGui.TableNextColumn(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); - var idx = 0; - foreach (var flag in Eqp.EqpAttributes[meta.Slot]) - { - using var id = ImRaii.PushId(idx++); - var defaultValue = defaultEntry.HasFlag(flag); - var currentValue = meta.Entry.HasFlag(flag); - if (Checkmark("##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value)) - editor.MetaEditor.Change(meta.Copy(value ? meta.Entry | flag : meta.Entry & ~flag)); - - ImGui.SameLine(); - } - - ImGui.NewLine(); - } - } - private static class EqdpRow - { - private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, - editor.MetaEditor.Eqdp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var raceCode = Names.CombinedRace(_new.Gender, _new.Race); - var validRaceCode = CharacterUtilityData.EqdpIdx(raceCode, false) >= 0; - var canAdd = validRaceCode && editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : - validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; - var defaultEntry = validRaceCode - ? ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) - : 0; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##eqdpId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), - _new.Slot.IsAccessory(), setId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); - } - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.Race("##eqdpRace", _new.Race, out var race)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), - _new.Slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - - ImGui.TableNextColumn(); - if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), - _new.Slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(GenderTooltip); - - ImGui.TableNextColumn(); - if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) - { - var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), - slot.IsAccessory(), _new.SetId); - _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); - } - - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - var (bit1, bit2) = defaultEntry.ToBits(_new.Slot); - Checkmark("Material##eqdpCheck1", string.Empty, bit1, bit1, out _); - ImGui.SameLine(); - Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _); - } - - public static void Draw(MetaFileManager metaFileManager, EqdpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Race.ToName()); - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Gender.ToName()); - ImGuiUtil.HoverTooltip(GenderTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToName()); - ImGuiUtil.HoverTooltip(EquipSlotTooltip); - - // Values - var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), - meta.SetId); - var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); - var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); - ImGui.TableNextColumn(); - if (Checkmark("Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1)) - editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, newBit1, bit2))); - - ImGui.SameLine(); - if (Checkmark("Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2)) - editor.MetaEditor.Change(meta.Copy(Eqdp.FromSlotAndBits(meta.Slot, bit1, newBit2))); - } - } - - private static class EstRow - { - private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstType.Body, 1, EstEntry.Zero); - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, - editor.MetaEditor.Est.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##estId", IdWidth, _new.SetId.Id, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); - _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - ImGui.TableNextColumn(); - if (Combos.Race("##estRace", _new.Race, out var race)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); - _new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - - ImGui.TableNextColumn(); - if (Combos.Gender("##estGender", _new.Gender, out var gender)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); - _new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(GenderTooltip); - - ImGui.TableNextColumn(); - if (Combos.EstSlot("##estSlot", _new.Slot, out var slot)) - { - var newDefaultEntry = EstFile.GetDefault(metaFileManager, slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); - _new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry); - } - - ImGuiUtil.HoverTooltip(EstTypeTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry.Value, defaultEntry.Value, out _, 0, ushort.MaxValue, 0.05f); - } - - public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Race.ToName()); - ImGuiUtil.HoverTooltip(ModelRaceTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Gender.ToName()); - ImGuiUtil.HoverTooltip(GenderTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Slot.ToString()); - ImGuiUtil.HoverTooltip(EstTypeTooltip); - - // Values - var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); - ImGui.TableNextColumn(); - if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry.Value, defaultEntry.Value, - out var entry, 0, ushort.MaxValue, 0.05f)) - editor.MetaEditor.Change(meta.Copy(new EstEntry((ushort)entry))); - } - } - private static class GmpRow - { - private static GmpManipulation _new = new(GmpEntry.Default, 1); - - private static float RotationWidth - => 75 * UiHelpers.Scale; - - private static float UnkWidth - => 50 * UiHelpers.Scale; - - private static float IdWidth - => 100 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, - editor.MetaEditor.Gmp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (IdInput("##gmpId", IdWidth, _new.SetId.Id, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) - _new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId); - - ImGuiUtil.HoverTooltip(ModelSetIdTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - Checkmark("##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _); - ImGui.TableNextColumn(); - Checkmark("##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _); - ImGui.TableNextColumn(); - IntDragInput("##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0, - 360, 0f); - ImGui.SameLine(); - IntDragInput("##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0, - 360, 0f); - ImGui.SameLine(); - IntDragInput("##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0, - 360, 0f); - ImGui.TableNextColumn(); - IntDragInput("##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f); - ImGui.SameLine(); - IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f); - } - - public static void Draw(MetaFileManager metaFileManager, GmpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SetId.ToString()); - ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); - - // Values - var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId); - ImGui.TableNextColumn(); - if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled })); - - ImGui.TableNextColumn(); - if (Checkmark("##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { Animated = animated })); - - ImGui.TableNextColumn(); - if (IntDragInput("##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth, - meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationA = (ushort)rotationA })); - - ImGui.SameLine(); - if (IntDragInput("##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth, - meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationB = (ushort)rotationB })); - - ImGui.SameLine(); - if (IntDragInput("##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth, - meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { RotationC = (ushort)rotationC })); - - ImGui.TableNextColumn(); - if (IntDragInput("##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA, - defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownA = (byte)unkA })); - - ImGui.SameLine(); - if (IntDragInput("##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB, - defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f)) - editor.MetaEditor.Change(meta.Copy(meta.Entry with { UnknownB = (byte)unkB })); - } - } - private static class RspRow - { - private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, RspEntry.One); - - private static float FloatWidth - => 150 * UiHelpers.Scale; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, - editor.MetaEditor.Rsp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already edited."; - var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new.Copy(defaultEntry)); - - // Identifier - ImGui.TableNextColumn(); - if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace)) - _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(metaFileManager, subRace, _new.Attribute)); - - ImGuiUtil.HoverTooltip(RacialTribeTooltip); - - ImGui.TableNextColumn(); - if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute)) - _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(metaFileManager, subRace, attribute)); - - ImGuiUtil.HoverTooltip(ScalingTypeTooltip); - - // Values - using var disabled = ImRaii.Disabled(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(FloatWidth); - var value = defaultEntry.Value; - ImGui.DragFloat("##rspValue", ref value, 0f); - } - - public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.SubRace.ToName()); - ImGuiUtil.HoverTooltip(RacialTribeTooltip); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImGui.TextUnformatted(meta.Attribute.ToFullString()); - ImGuiUtil.HoverTooltip(ScalingTypeTooltip); - ImGui.TableNextColumn(); - - // Values - var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute).Value; - var value = meta.Entry.Value; - ImGui.SetNextItemWidth(FloatWidth); - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), - def != value); - if (ImGui.DragFloat("##rspValue", ref value, 0.001f, RspEntry.MinValue, RspEntry.MaxValue) - && value is >= RspEntry.MinValue and <= RspEntry.MaxValue) - editor.MetaEditor.Change(meta.Copy(new RspEntry(value))); - - ImGuiUtil.HoverTooltip($"Default Value: {def:0.###}"); - } - } - private static class GlobalEqpRow - { - private static GlobalEqpManipulation _new = new() - { - Type = GlobalEqpType.DoNotHideEarrings, - Condition = 1, - }; - - public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize) - { - ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current global EQP manipulations to clipboard.", iconSize, - editor.MetaEditor.GlobalEqp.Select(m => (MetaManipulation)m)); - ImGui.TableNextColumn(); - var canAdd = editor.MetaEditor.CanAdd(_new); - var tt = canAdd ? "Stage this edit." : "This entry is already manipulated."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) - editor.MetaEditor.Add(_new); - - // Identifier - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(250 * ImUtf8.GlobalScale); - using (var combo = ImUtf8.Combo("##geqpType"u8, _new.Type.ToName())) - { - if (combo) - foreach (var type in Enum.GetValues()) - { - if (ImUtf8.Selectable(type.ToName(), type == _new.Type)) - _new = new GlobalEqpManipulation - { - Type = type, - Condition = type.HasCondition() ? _new.Type.HasCondition() ? _new.Condition : 1 : 0, - }; - ImUtf8.HoverTooltip(type.ToDescription()); - } - } - - ImUtf8.HoverTooltip(_new.Type.ToDescription()); - - ImGui.TableNextColumn(); - if (!_new.Type.HasCondition()) - return; - - if (IdInput("##geqpCond", 100 * ImUtf8.GlobalScale, _new.Condition.Id, out var newId, 1, ushort.MaxValue, _new.Condition.Id <= 1)) - _new = _new with { Condition = newId }; - ImUtf8.HoverTooltip("The Model ID for the item that should not be hidden."u8); - } - - public static void Draw(MetaFileManager metaFileManager, GlobalEqpManipulation meta, ModEditor editor, Vector2 iconSize) - { - DrawMetaButtons(meta, editor, iconSize); - ImGui.TableNextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImUtf8.Text(meta.Type.ToName()); - ImUtf8.HoverTooltip(meta.Type.ToDescription()); - ImGui.TableNextColumn(); - if (meta.Type.HasCondition()) - { - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); - ImUtf8.Text($"{meta.Condition.Id}"); - } - } - } -#endif - - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. - private static bool IdInput(string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border) - { - int tmp = currentId; - ImGui.SetNextItemWidth(width); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border); - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, border); - if (ImGui.InputInt(label, ref tmp, 0)) - tmp = Math.Clamp(tmp, minId, maxId); - - newId = (ushort)tmp; - return newId != currentId; - } - - // A checkmark that compares against a default value and shows a tooltip. - // Returns true if newValue is changed against currentValue. - private static bool Checkmark(string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue) - { - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - newValue = currentValue; - ImGui.Checkbox(label, ref newValue); - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - return newValue != currentValue; - } - - // A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max. - // Returns true if newValue changed against currentValue. - private static bool IntDragInput(string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue, - int minValue, int maxValue, float speed) - { - newValue = currentValue; - using var color = ImRaii.PushColor(ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), - defaultValue != currentValue); - ImGui.SetNextItemWidth(width); - if (ImGui.DragInt(label, ref newValue, speed, minValue, maxValue)) - newValue = Math.Clamp(newValue, minValue, maxValue); - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return newValue != currentValue; - } - private static void CopyToClipboardButton(string tooltip, Vector2 iconSize, MetaDictionary manipulations) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaManipulation.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); if (text.Length > 0) ImGui.SetClipboardText(text); } @@ -731,8 +119,11 @@ public partial class ModEditWindow var clipboard = ImGuiUtil.GetClipboardText(); var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaManipulation.CurrentVersion && manips != null) + if (version == MetaApi.CurrentVersion && manips != null) + { _editor.MetaEditor.UpdateTo(manips); + _editor.MetaEditor.Changes = true; + } } ImGuiUtil.HoverTooltip( @@ -745,194 +136,14 @@ public partial class ModEditWindow { var clipboard = ImGuiUtil.GetClipboardText(); var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaManipulation.CurrentVersion && manips != null) + if (version == MetaApi.CurrentVersion && manips != null) + { _editor.MetaEditor.SetTo(manips); + _editor.MetaEditor.Changes = true; + } } ImGuiUtil.HoverTooltip( "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); } - - private static void DrawMetaButtons(MetaManipulation meta, ModEditor editor, Vector2 iconSize) - { - //ImGui.TableNextColumn(); - //CopyToClipboardButton("Copy this manipulation to clipboard.", iconSize, Array.Empty().Append(meta)); - // - //ImGui.TableNextColumn(); - //if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true)) - // editor.MetaEditor.Delete(meta); - } -} - - -public interface IMetaDrawer -{ - public void Draw(); } - - - - -public abstract class MetaDrawer(ModEditor editor, MetaFileManager metaFiles) : IMetaDrawer - where TIdentifier : unmanaged, IMetaIdentifier - where TEntry : unmanaged -{ - protected readonly ModEditor Editor = editor; - protected readonly MetaFileManager MetaFiles = metaFiles; - protected TIdentifier Identifier; - protected TEntry Entry; - private bool _initialized; - - public void Draw() - { - if (!_initialized) - { - Initialize(); - _initialized = true; - } - - DrawNew(); - foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) - { - using var id = ImUtf8.PushId(idx); - DrawEntry(identifier, entry); - } - } - - protected abstract void DrawNew(); - protected abstract void Initialize(); - protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); - - protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); -} - - -#if false -public sealed class GmpMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new GmpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(GmpIdentifier identifier, GmpEntry entry) - { } - - protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class EstMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(EstIdentifier identifier, EstEntry entry) - { } - - protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class EqdpMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqdpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(EqdpIdentifier identifier, EqdpEntry entry) - { } - - protected override IEnumerable<(EqdpIdentifier, EqdpEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class EqpMetaDrawer(ModEditor editor, MetaFileManager metaManager) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(EqpIdentifier identifier, EqpEntry entry) - { } - - protected override IEnumerable<(EqpIdentifier, EqpEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - -public sealed class RspMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new RspIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(RspIdentifier identifier, RspEntry entry) - { } - - protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} - - - -public sealed class GlobalEqpMetaDrawer(ModEditor editor) : MetaDrawer, IService -{ - protected override void Initialize() - { - Identifier = new EqpIdentifier(1, EquipSlot.Body); - UpdateEntry(); - } - - private void UpdateEntry() - => Entry = ExpandedEqpFile.GetDefault(metaManager, Identifier.SetId); - - protected override void DrawNew() - { } - - protected override void DrawEntry(GlobalEqpManipulation identifier, byte _) - { } - - protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() - => editor.MetaEditor.Eqp.Select(kvp => (kvp.Key, kvp.Value.ToEntry(kvp.Key.Slot))); -} -#endif diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 1f935eb6..72ab37b2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -95,7 +95,7 @@ public partial class ModEditWindow task.ContinueWith(t => { GamePaths = FinalizeIo(t); }, TaskScheduler.Default); } - private EstManipulation[] GetCurrentEstManipulations() + private KeyValuePair[] GetCurrentEstManipulations() { var mod = _edit._editor.Mod; var option = _edit._editor.Option; @@ -106,9 +106,7 @@ public partial class ModEditWindow return mod.AllDataContainers .Where(subMod => subMod != option) .Prepend(option) - .SelectMany(subMod => subMod.Manipulations) - .Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est) - .Select(manipulation => manipulation.Est) + .SelectMany(subMod => subMod.Manipulations.Est) .ToArray(); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index af01047b..9410b793 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -25,6 +25,7 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; using Penumbra.Util; using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; @@ -586,7 +587,7 @@ public partial class ModEditWindow : Window, IDisposable StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor) + CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers) : base(WindowBaseLabel) { _performance = performance; @@ -606,6 +607,7 @@ public partial class ModEditWindow : Window, IDisposable _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; + _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 5e3aac48..962d156d 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -120,9 +120,9 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy { var _ = data switch { - Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, - MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), - _ => false, + Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, + IMetaIdentifier m => ImGui.Selectable(m.ToString()), + _ => false, }; } } diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 37561000..7076b80f 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -98,7 +98,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH // Filters mean we can not use the known counts. if (hasFilters) { - var it2 = m.Select(p => (p.Key.ToString(), p.Value.Name)); + var it2 = m.IdentifierSources.Select(p => (p.Item1.ToString(), p.Item2.Name)); if (stop >= 0) { ImGuiClip.DrawEndDummy(stop + it2.Count(CheckFilters), height); @@ -117,7 +117,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH } else { - stop = ImGuiClip.ClippedDraw(m, skips, DrawLine, m.Count, ~stop); + stop = ImGuiClip.ClippedDraw(m.IdentifierSources, skips, DrawLine, m.Count, ~stop); ImGuiClip.DrawEndDummy(stop, height); } } @@ -152,11 +152,11 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(name); + ImGuiUtil.CopyOnClickSelectable(name.Text); } /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine(KeyValuePair pair) + private static void DrawLine((IMetaIdentifier, IMod) pair) { var (manipulation, mod) = pair; ImGui.TableNextColumn(); @@ -165,7 +165,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(mod.Name); + ImGuiUtil.CopyOnClickSelectable(mod.Name.Text); } /// Check filters for file replacements. diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 0647ea8e..cb43ac06 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -2,7 +2,6 @@ using OtterGui.Classes; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; @@ -10,103 +9,14 @@ namespace Penumbra.Util; public static class IdentifierExtensions { - /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. - public static void MetaChangedItems(this ObjectIdentification identifier, IDictionary changedItems, - MetaManipulation manip) - { - switch (manip.ManipulationType) - { - case MetaManipulation.Type.Imc: - switch (manip.Imc.ObjectType) - { - case ObjectType.Equipment: - case ObjectType.Accessory: - identifier.Identify(changedItems, - GamePaths.Equipment.Mtrl.Path(manip.Imc.PrimaryId, GenderRace.MidlanderMale, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Weapon: - identifier.Identify(changedItems, - GamePaths.Weapon.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - case ObjectType.DemiHuman: - identifier.Identify(changedItems, - GamePaths.DemiHuman.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.EquipSlot, manip.Imc.Variant, - "a")); - break; - case ObjectType.Monster: - identifier.Identify(changedItems, - GamePaths.Monster.Mtrl.Path(manip.Imc.PrimaryId, manip.Imc.SecondaryId, manip.Imc.Variant, "a")); - break; - } - - break; - case MetaManipulation.Type.Eqdp: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Eqdp.SetId, Names.CombinedRace(manip.Eqdp.Gender, manip.Eqdp.Race), manip.Eqdp.Slot)); - break; - case MetaManipulation.Type.Eqp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Eqp.SetId, GenderRace.MidlanderMale, manip.Eqp.Slot)); - break; - case MetaManipulation.Type.Est: - switch (manip.Est.Slot) - { - case EstType.Hair: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Hair (Hair) {manip.Est.SetId}", null); - break; - case EstType.Face: - changedItems.TryAdd($"Customization: {manip.Est.Race} {manip.Est.Gender} Face (Face) {manip.Est.SetId}", null); - break; - case EstType.Body: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Body)); - break; - case EstType.Head: - identifier.Identify(changedItems, - GamePaths.Equipment.Mdl.Path(manip.Est.SetId, Names.CombinedRace(manip.Est.Gender, manip.Est.Race), - EquipSlot.Head)); - break; - } - - break; - case MetaManipulation.Type.Gmp: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(manip.Gmp.SetId, GenderRace.MidlanderMale, EquipSlot.Head)); - break; - case MetaManipulation.Type.Rsp: - changedItems.TryAdd($"{manip.Rsp.SubRace.ToName()} {manip.Rsp.Attribute.ToFullString()}", null); - break; - case MetaManipulation.Type.GlobalEqp: - var path = manip.GlobalEqp.Type switch - { - GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Ears), - GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Neck), - GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.Wrists), - GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.RFinger), - GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(manip.GlobalEqp.Condition, GenderRace.MidlanderMale, - EquipSlot.LFinger), - GlobalEqpType.DoNotHideHrothgarHats => string.Empty, - GlobalEqpType.DoNotHideVieraHats => string.Empty, - _ => string.Empty, - }; - if (path.Length > 0) - identifier.Identify(changedItems, path); - break; - } - } - public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); - foreach (var manip in container.Manipulations) - MetaChangedItems(identifier, changedItems, manip); + foreach (var manip in container.Manipulations.Identifiers) + manip.AddChangedItems(identifier, changedItems); } public static void RemoveMachinistOffhands(this SortedList changedItems) From 4ca49598f8ffff55728e98e108183c88d679c2e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 17:40:47 +0200 Subject: [PATCH 171/865] Small improvement. --- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index 962d156d..c1a3c1eb 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; @@ -116,15 +117,12 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy using var indent = ImRaii.PushIndent(30f); foreach (var data in conflict.Conflicts) { - unsafe + _ = data switch { - var _ = data switch - { - Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, - IMetaIdentifier m => ImGui.Selectable(m.ToString()), - _ => false, - }; - } + Utf8GamePath p => ImUtf8.Selectable(p.Path.Span, false), + IMetaIdentifier m => ImUtf8.Selectable(m.ToString(), false), + _ => false, + }; } return true; From e33512cf7ff3dcc887b0516188e79d6d9fe09aa1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Jun 2024 23:09:38 +0200 Subject: [PATCH 172/865] Fix issue, remove IMetaCache. --- Penumbra/Collections/Cache/IMetaCache.cs | 14 +++----------- Penumbra/Meta/Manipulations/MetaDictionary.cs | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs index 218c1840..eacbdcc2 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -4,21 +4,12 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; -public interface IMetaCache : IDisposable -{ - public void SetFiles(); - public void Reset(); - public void ResetFiles(); - - public int Count { get; } -} - public abstract class MetaCacheBase - : Dictionary, IMetaCache + : Dictionary where TIdentifier : unmanaged, IMetaIdentifier where TEntry : unmanaged { - public MetaCacheBase(MetaFileManager manager, ModCollection collection) + protected MetaCacheBase(MetaFileManager manager, ModCollection collection) { Manager = manager; Collection = collection; @@ -76,6 +67,7 @@ public abstract class MetaCacheBase { IncorporateChangesInternal(); } + if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods) SetFiles(); } diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 5a51df83..1093c6c5 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -346,7 +346,7 @@ public class MetaDictionary } failedIdentifier = default; - return false; + return true; } public void SetTo(MetaDictionary other) From ad0c64d4ac9e3b2d8ba89b7920b2f96a72480709 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 11:37:39 +0200 Subject: [PATCH 173/865] Change Eqp hook to not need eqp files anymore. --- Penumbra/Collections/Cache/EqpCache.cs | 12 ++++++++++++ Penumbra/Collections/Cache/MetaCache.cs | 8 -------- Penumbra/Collections/ModCollection.Cache.Access.cs | 7 ------- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 7 +++---- Penumbra/Interop/PathResolving/MetaState.cs | 5 ++--- Penumbra/UI/ConfigWindow.cs | 3 ++- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 599ae588..8dde9aba 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,3 +1,4 @@ +using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; @@ -28,6 +29,17 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations."); } + public unsafe EqpEntry GetValues(CharacterArmor* armor) + => GetSingleValue(armor[0].Set, EquipSlot.Head) + | GetSingleValue(armor[1].Set, EquipSlot.Body) + | GetSingleValue(armor[2].Set, EquipSlot.Hands) + | GetSingleValue(armor[3].Set, EquipSlot.Legs) + | GetSingleValue(armor[4].Set, EquipSlot.Feet); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) + => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(manager, id) & Eqp.Mask(slot); + public MetaList.MetaReverter TemporarilySetFile() => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index bc6ef34d..45a85d0f 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,3 @@ -using System.IO.Pipes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Services; @@ -146,9 +145,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public void SetImcFiles(bool fromFullCompute) => Imc.SetFiles(fromFullCompute); - public MetaList.MetaReverter TemporarilySetEqpFile() - => Eqp.TemporarilySetFile(); - public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => Eqdp.TemporarilySetFile(genderRace, accessory); @@ -161,10 +157,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter TemporarilySetEstFile(EstType type) => Est.TemporarilySetFiles(type); - public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) - => GlobalEqp.Apply(baseEntry, armor); - - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 484d4dd2..3e3386ea 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -100,10 +100,6 @@ public partial class ModCollection return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; } - public MetaList.MetaReverter TemporarilySetEqpFile(CharacterUtility utility) - => _cache?.Meta.TemporarilySetEqpFile() - ?? utility.TemporarilyResetResource(MetaIndex.Eqp); - public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetGmpFile() ?? utility.TemporarilyResetResource(MetaIndex.Gmp); @@ -115,7 +111,4 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type) => _cache?.Meta.TemporarilySetEstFile(type) ?? utility.TemporarilyResetResource((MetaIndex)type); - - public unsafe EqpEntry ApplyGlobalEqp(EqpEntry baseEntry, CharacterArmor* armor) - => _cache?.Meta.ApplyGlobalEqp(baseEntry, armor) ?? baseEntry; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 6663c211..448605c1 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -19,11 +19,10 @@ public unsafe class EqpHook : FastHook private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) { - if (_metaState.EqpCollection.Valid) + if (_metaState.EqpCollection is { Valid: true, ModCollection.MetaCache: { } cache }) { - using var eqp = _metaState.ResolveEqpData(_metaState.EqpCollection.ModCollection); - Task.Result.Original(utility, flags, armor); - *flags = _metaState.EqpCollection.ModCollection.ApplyGlobalEqp(*flags, armor); + *flags = cache.Eqp.GetValues(armor); + *flags = cache.GlobalEqp.Apply(*flags, armor); } else { diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 6fa5c263..de7912e0 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -47,6 +47,8 @@ public sealed unsafe class MetaState : IDisposable public ResolveData CustomizeChangeCollection = ResolveData.Invalid; public ResolveData EqpCollection = ResolveData.Invalid; + public ResolveData GmpCollection = ResolveData.Invalid; + public PrimaryId UndividedGmpId = 0; private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; @@ -93,9 +95,6 @@ public sealed unsafe class MetaState : IDisposable _ => DisposableContainer.Empty, }; - public MetaList.MetaReverter ResolveEqpData(ModCollection collection) - => collection.TemporarilySetEqpFile(_characterUtility); - public MetaList.MetaReverter ResolveGmpData(ModCollection collection) => collection.TemporarilySetGmpFile(_characterUtility); diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 9ae11fc3..0ae16f6d 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Services; using Penumbra.UI.Classes; @@ -144,7 +145,7 @@ public sealed class ConfigWindow : Window using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); ImGui.NewLine(); - ImGuiUtil.TextWrapped(text); + ImUtf8.TextWrapped(text); color.Pop(); ImGui.NewLine(); From 30b32fdcd21e82d9f9e1176793f6b01082a79528 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 12:09:41 +0200 Subject: [PATCH 174/865] Fix EQDP bug. --- Penumbra/Collections/Cache/EqpCache.cs | 2 +- Penumbra/Meta/Files/EqpGmpFile.cs | 12 ++---------- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 8dde9aba..32c4c0ae 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -38,7 +38,7 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) - => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(manager, id) & Eqp.Mask(slot); + => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot); public MetaList.MetaReverter TemporarilySetFile() => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index 17541c4f..a7470b75 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -104,15 +104,11 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } } -public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable +public sealed class ExpandedEqpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, false), IEnumerable { public static readonly CharacterUtility.InternalIndex InternalIndex = CharacterUtility.ReverseIndices[(int)MetaIndex.Eqp]; - public ExpandedEqpFile(MetaFileManager manager) - : base(manager, false) - { } - public EqpEntry this[PrimaryId idx] { get => (EqpEntry)GetInternal(idx); @@ -147,15 +143,11 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable => GetEnumerator(); } -public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable +public sealed class ExpandedGmpFile(MetaFileManager manager) : ExpandedEqpGmpBase(manager, true), IEnumerable { public static readonly CharacterUtility.InternalIndex InternalIndex = CharacterUtility.ReverseIndices[(int)MetaIndex.Gmp]; - public ExpandedGmpFile(MetaFileManager manager) - : base(manager, true) - { } - public GmpEntry this[PrimaryId idx] { get => new() { Value = GetInternal(idx) }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index b1ac93f1..970b70cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -112,7 +112,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil ImGui.SameLine(); if (Checkmark("Model##eqdp"u8, "\0"u8, entry.Model, defaultEntry.Model, out var newModel)) { - entry = entry with { Material = newModel }; + entry = entry with { Model = newModel }; changes = true; } From a61a96f1ef5bcc9e0c96210595c07a52c0ac8115 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 14:04:16 +0200 Subject: [PATCH 175/865] Make GmpEntry readonly. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 0a2e2650..cf1ff07e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0a2e2650d693d1bba267498f96112682cc09dbab +Subproject commit cf1ff07e900e2f93ab628a1fa535fc2b103794a5 From a7b90639c6deb2859adeac608521bd664ac2cbca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 14:46:42 +0200 Subject: [PATCH 176/865] Some fixes. --- Penumbra/Collections/Cache/EqdpCache.cs | 10 ++++++---- Penumbra/Collections/Cache/EstCache.cs | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index f3475c7e..7139bb72 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,4 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -28,7 +27,10 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) } public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Eqp); + { + foreach (var t in CharacterUtilityData.EqdpIndices) + Manager.SetFile(null, t); + } protected override void IncorporateChangesInternal() { @@ -89,8 +91,8 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) var mask = Eqdp.Mask(identifier.Slot); if ((origEntry & mask) == entry) return false; - - file[identifier.SetId] = (entry & ~mask) | origEntry; + + file[identifier.SetId] = (origEntry & ~mask) | entry; return true; } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 412dd322..8ee530cc 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -55,7 +55,12 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) } public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Eqp); + { + Manager.SetFile(null, MetaIndex.FaceEst); + Manager.SetFile(null, MetaIndex.HairEst); + Manager.SetFile(null, MetaIndex.BodyEst); + Manager.SetFile(null, MetaIndex.HeadEst); + } protected override void IncorporateChangesInternal() { From c53f29c257003bb8131441cc5bcf7086d808ad73 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 15:19:48 +0200 Subject: [PATCH 177/865] Fix unnecessary EST file creations. --- Penumbra/Collections/Cache/EstCache.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 8ee530cc..845ff128 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -74,7 +74,7 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) public EstEntry GetEstEntry(EstIdentifier identifier) { - var file = GetFile(identifier); + var file = GetCurrentFile(identifier); return file != null ? file[identifier.GenderRace, identifier.SetId] : EstFile.GetDefault(Manager, identifier); @@ -124,9 +124,19 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) Clear(); } + private EstFile? GetCurrentFile(EstIdentifier identifier) + => identifier.Slot switch + { + EstType.Hair => _estHairFile, + EstType.Face => _estFaceFile, + EstType.Body => _estBodyFile, + EstType.Head => _estHeadFile, + _ => null, + }; + private EstFile? GetFile(EstIdentifier identifier) { - if (Manager.CharacterUtility.Ready) + if (!Manager.CharacterUtility.Ready) return null; return identifier.Slot switch From 943207cae8923720fc8dbe56d763ddc3d1034bd4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 15:55:51 +0200 Subject: [PATCH 178/865] Make GMP independent of file, cleanup unused functions. --- Penumbra/Collections/Cache/EqdpCache.cs | 4 +- Penumbra/Collections/Cache/EqpCache.cs | 59 ++--------------- Penumbra/Collections/Cache/EstCache.cs | 4 +- Penumbra/Collections/Cache/GmpCache.cs | 59 ++--------------- Penumbra/Collections/Cache/IMetaCache.cs | 2 - Penumbra/Collections/Cache/ImcCache.cs | 4 +- Penumbra/Collections/Cache/MetaCache.cs | 3 - Penumbra/Collections/Cache/RspCache.cs | 5 +- .../Collections/ModCollection.Cache.Access.cs | 4 -- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 64 +++++++++++++++++++ Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 24 +++---- Penumbra/Interop/PathResolving/MetaState.cs | 3 - Penumbra/Interop/Services/MetaList.cs | 23 ++----- Penumbra/Meta/Files/EqpGmpFile.cs | 8 +-- 14 files changed, 108 insertions(+), 158 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/GmpHook.cs diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 7139bb72..c63403ae 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -26,7 +26,7 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) Manager.SetFile(_eqdpFiles[i], index); } - public override void ResetFiles() + public void ResetFiles() { foreach (var t in CharacterUtilityData.EqdpIndices) Manager.SetFile(null, t); @@ -62,7 +62,7 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) return Manager.TemporarilySetFile(_eqdpFiles[i], idx); } - public override void Reset() + public void Reset() { foreach (var file in _eqdpFiles.OfType()) { diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 32c4c0ae..b1e03943 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -1,7 +1,5 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -10,24 +8,11 @@ namespace Penumbra.Collections.Cache; public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedEqpFile? _eqpFile; - public override void SetFiles() - => Manager.SetFile(_eqpFile, MetaIndex.Eqp); - - public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Eqp); + { } protected override void IncorporateChangesInternal() - { - if (GetFile() is not { } file) - return; - - foreach (var (identifier, (_, entry)) in this) - Apply(file, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQP manipulations."); - } + { } public unsafe EqpEntry GetValues(CharacterArmor* armor) => GetSingleValue(armor[0].Set, EquipSlot.Head) @@ -40,29 +25,14 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) => TryGetValue(new EqpIdentifier(id, slot), out var pair) ? pair.Entry : ExpandedEqpFile.GetDefault(Manager, id) & Eqp.Mask(slot); - public MetaList.MetaReverter TemporarilySetFile() - => Manager.TemporarilySetFile(_eqpFile, MetaIndex.Eqp); - - public override void Reset() - { - if (_eqpFile == null) - return; - - _eqpFile.Reset(Keys.Select(identifier => identifier.SetId)); - Clear(); - } + public void Reset() + => Clear(); protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry) - { - if (GetFile() is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(EqpIdentifier identifier) - { - if (GetFile() is { } file) - Apply(file, identifier, ExpandedEqpFile.GetDefault(Manager, identifier.SetId)); - } + { } public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry) { @@ -76,20 +46,5 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) } protected override void Dispose(bool _) - { - _eqpFile?.Dispose(); - _eqpFile = null; - Clear(); - } - - private ExpandedEqpFile? GetFile() - { - if (_eqpFile != null) - return _eqpFile; - - if (!Manager.CharacterUtility.Ready) - return null; - - return _eqpFile = new ExpandedEqpFile(Manager); - } + => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 845ff128..6a9fa909 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -54,7 +54,7 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) return Manager.TemporarilySetFile(file, idx); } - public override void ResetFiles() + public void ResetFiles() { Manager.SetFile(null, MetaIndex.FaceEst); Manager.SetFile(null, MetaIndex.HairEst); @@ -80,7 +80,7 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) : EstFile.GetDefault(Manager, identifier); } - public override void Reset() + public void Reset() { _estFaceFile?.Reset(); _estHairFile?.Reset(); diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 1475ffd5..0beb51d8 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,6 +1,4 @@ using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -9,48 +7,20 @@ namespace Penumbra.Collections.Cache; public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private ExpandedGmpFile? _gmpFile; - public override void SetFiles() - => Manager.SetFile(_gmpFile, MetaIndex.Gmp); - - public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.Gmp); + { } protected override void IncorporateChangesInternal() - { - if (GetFile() is not { } file) - return; + { } - foreach (var (identifier, (_, entry)) in this) - Apply(file, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed GMP manipulations."); - } - - public MetaList.MetaReverter TemporarilySetFile() - => Manager.TemporarilySetFile(_gmpFile, MetaIndex.Gmp); - - public override void Reset() - { - if (_gmpFile == null) - return; - - _gmpFile.Reset(Keys.Select(identifier => identifier.SetId)); - Clear(); - } + public void Reset() + => Clear(); protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry) - { - if (GetFile() is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(GmpIdentifier identifier) - { - if (GetFile() is { } file) - Apply(file, identifier, ExpandedGmpFile.GetDefault(Manager, identifier.SetId)); - } + { } public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry) { @@ -63,20 +33,5 @@ public sealed class GmpCache(MetaFileManager manager, ModCollection collection) } protected override void Dispose(bool _) - { - _gmpFile?.Dispose(); - _gmpFile = null; - Clear(); - } - - private ExpandedGmpFile? GetFile() - { - if (_gmpFile != null) - return _gmpFile; - - if (!Manager.CharacterUtility.Ready) - return null; - - return _gmpFile = new ExpandedGmpFile(Manager); - } + => Clear(); } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs index eacbdcc2..dd218b48 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -27,8 +27,6 @@ public abstract class MetaCacheBase } public abstract void SetFiles(); - public abstract void Reset(); - public abstract void ResetFiles(); public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) { diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 3d05e793..a9daf795 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -38,7 +38,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection)); } - public override void ResetFiles() + public void ResetFiles() { foreach (var (path, _) in _imcFiles) Collection._cache!.ForceFile(path, FullPath.Empty); @@ -56,7 +56,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) } - public override void Reset() + public void Reset() { foreach (var (path, (file, set)) in _imcFiles) { diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 45a85d0f..c8a116eb 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -148,9 +148,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => Eqdp.TemporarilySetFile(genderRace, accessory); - public MetaList.MetaReverter TemporarilySetGmpFile() - => Gmp.TemporarilySetFile(); - public MetaList.MetaReverter TemporarilySetCmpFile() => Rsp.TemporarilySetFile(); diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs index 3889d6f1..8a5fe97d 100644 --- a/Penumbra/Collections/Cache/RspCache.cs +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -13,9 +13,6 @@ public sealed class RspCache(MetaFileManager manager, ModCollection collection) public override void SetFiles() => Manager.SetFile(_cmpFile, MetaIndex.HumanCmp); - public override void ResetFiles() - => Manager.SetFile(null, MetaIndex.HumanCmp); - protected override void IncorporateChangesInternal() { if (GetFile() is not { } file) @@ -30,7 +27,7 @@ public sealed class RspCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter TemporarilySetFile() => Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); - public override void Reset() + public void Reset() { if (_cmpFile == null) return; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3e3386ea..8701e3bb 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -100,10 +100,6 @@ public partial class ModCollection return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; } - public MetaList.MetaReverter TemporarilySetGmpFile(CharacterUtility utility) - => _cache?.Meta.TemporarilySetGmpFile() - ?? utility.TemporarilyResetResource(MetaIndex.Gmp); - public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetCmpFile() ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs new file mode 100644 index 00000000..60966fb7 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -0,0 +1,64 @@ +using OtterGui.Services; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class GmpHook : FastHook +{ + public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + + private readonly MetaState _metaState; + + private static readonly Finalizer StablePointer = new(); + + public GmpHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, true); + } + + /// + /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . + /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, + /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. + /// + private nint Detour(nint gmpResource, uint dividedHeadId) + { + nint ret; + if (_metaState.GmpCollection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Gmp.TryGetValue(new GmpIdentifier(_metaState.UndividedGmpId), out var entry)) + { + if (entry.Entry.Enabled) + { + *StablePointer.Pointer = entry.Entry.Value; + // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. + // We then go backwards from our pointer because this gets added by the calling functions. + ret = (nint)(StablePointer.Pointer - (_metaState.UndividedGmpId.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); + } + else + { + ret = nint.Zero; + } + } + else + { + ret = Task.Result.Original(gmpResource, dividedHeadId); + } + + Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + return ret; + } + + /// Allocate and clean up our single stable ulong pointer. + private class Finalizer + { + public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); + + ~Finalizer() + { + Marshal.FreeHGlobal((nint)Pointer); + } + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index e451f118..a3e56d7f 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -1,10 +1,11 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + /// /// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself, /// but it only applies a changed gmp file after a redraw for some reason. @@ -26,10 +27,11 @@ public sealed unsafe class SetupVisor : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState) { - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var gmp = _metaState.ResolveGmpData(collection.ModCollection); - var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); - Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + _metaState.GmpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.UndividedGmpId = modelId; + var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); + Penumbra.Log.Information($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + _metaState.GmpCollection = ResolveData.Invalid; return ret; } } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index de7912e0..c8ebe18f 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -95,9 +95,6 @@ public sealed unsafe class MetaState : IDisposable _ => DisposableContainer.Empty, }; - public MetaList.MetaReverter ResolveGmpData(ModCollection collection) - => collection.TemporarilySetGmpFile(_characterUtility); - public MetaList.MetaReverter ResolveRspData(ModCollection collection) => collection.TemporarilySetCmpFile(_characterUtility); diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index e956040b..dc472b8e 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -102,30 +102,19 @@ public unsafe class MetaList : IDisposable ResetResourceInternal(); } - public sealed class MetaReverter : IDisposable + public sealed class MetaReverter(MetaList metaList, nint data, int length) : IDisposable { public static readonly MetaReverter Disabled = new(null!) { Disposed = true }; - public readonly MetaList MetaList; - public readonly nint Data; - public readonly int Length; + public readonly MetaList MetaList = metaList; + public readonly nint Data = data; + public readonly int Length = length; public readonly bool Resetter; public bool Disposed; - public MetaReverter(MetaList metaList, nint data, int length) - { - MetaList = metaList; - Data = data; - Length = length; - } - public MetaReverter(MetaList metaList) - { - MetaList = metaList; - Data = nint.Zero; - Length = 0; - Resetter = true; - } + : this(metaList, nint.Zero, 0) + => Resetter = true; public void Dispose() { diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index a7470b75..c47c84ef 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -15,10 +15,10 @@ namespace Penumbra.Meta.Files; /// public unsafe class ExpandedEqpGmpBase : MetaBaseFile { - protected const int BlockSize = 160; - protected const int NumBlocks = 64; - protected const int EntrySize = 8; - protected const int MaxSize = BlockSize * NumBlocks * EntrySize; + public const int BlockSize = 160; + public const int NumBlocks = 64; + public const int EntrySize = 8; + public const int MaxSize = BlockSize * NumBlocks * EntrySize; public const int Count = BlockSize * NumBlocks; From ebef4ff650c19aee26306135ab125a451210bab9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 16:17:44 +0200 Subject: [PATCH 179/865] No EST files anymore. --- Penumbra/Collections/Cache/EqpCache.cs | 11 -- Penumbra/Collections/Cache/EstCache.cs | 136 ++---------------- Penumbra/Collections/Cache/GmpCache.cs | 11 -- Penumbra/Collections/Cache/MetaCache.cs | 6 - .../Collections/ModCollection.Cache.Access.cs | 4 - Penumbra/Interop/Hooks/Meta/EstHook.cs | 49 +++++++ .../Hooks/Resources/ResolvePathHooksBase.cs | 29 ++-- Penumbra/Interop/PathResolving/MetaState.cs | 5 +- 8 files changed, 68 insertions(+), 183 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/EstHook.cs diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index b1e03943..7ba0c489 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -34,17 +34,6 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(EqpIdentifier identifier) { } - public static bool Apply(ExpandedEqpFile file, EqpIdentifier identifier, EqpEntry entry) - { - var origEntry = file[identifier.SetId]; - var mask = Eqp.Mask(identifier.Slot); - if ((origEntry & mask) == entry) - return false; - - file[identifier.SetId] = (origEntry & ~mask) | entry; - return true; - } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index 6a9fa909..ff94324e 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -1,5 +1,3 @@ -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -8,144 +6,26 @@ namespace Penumbra.Collections.Cache; public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private EstFile? _estFaceFile; - private EstFile? _estHairFile; - private EstFile? _estBodyFile; - private EstFile? _estHeadFile; - public override void SetFiles() - { - Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); - Manager.SetFile(_estHairFile, MetaIndex.HairEst); - Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); - Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); - } - - public void SetFile(MetaIndex index) - { - switch (index) - { - case MetaIndex.FaceEst: - Manager.SetFile(_estFaceFile, MetaIndex.FaceEst); - break; - case MetaIndex.HairEst: - Manager.SetFile(_estHairFile, MetaIndex.HairEst); - break; - case MetaIndex.BodyEst: - Manager.SetFile(_estBodyFile, MetaIndex.BodyEst); - break; - case MetaIndex.HeadEst: - Manager.SetFile(_estHeadFile, MetaIndex.HeadEst); - break; - } - } - - public MetaList.MetaReverter TemporarilySetFiles(EstType type) - { - var (file, idx) = type switch - { - EstType.Face => (_estFaceFile, MetaIndex.FaceEst), - EstType.Hair => (_estHairFile, MetaIndex.HairEst), - EstType.Body => (_estBodyFile, MetaIndex.BodyEst), - EstType.Head => (_estHeadFile, MetaIndex.HeadEst), - _ => (null, 0), - }; - - return Manager.TemporarilySetFile(file, idx); - } - - public void ResetFiles() - { - Manager.SetFile(null, MetaIndex.FaceEst); - Manager.SetFile(null, MetaIndex.HairEst); - Manager.SetFile(null, MetaIndex.BodyEst); - Manager.SetFile(null, MetaIndex.HeadEst); - } + { } protected override void IncorporateChangesInternal() - { - if (!Manager.CharacterUtility.Ready) - return; - - foreach (var (identifier, (_, entry)) in this) - Apply(GetFile(identifier)!, identifier, entry); - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EST manipulations."); - } + { } public EstEntry GetEstEntry(EstIdentifier identifier) - { - var file = GetCurrentFile(identifier); - return file != null - ? file[identifier.GenderRace, identifier.SetId] + => TryGetValue(identifier, out var entry) + ? entry.Entry : EstFile.GetDefault(Manager, identifier); - } public void Reset() - { - _estFaceFile?.Reset(); - _estHairFile?.Reset(); - _estBodyFile?.Reset(); - _estHeadFile?.Reset(); - Clear(); - } + => Clear(); protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry) - { - if (GetFile(identifier) is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(EstIdentifier identifier) - { - if (GetFile(identifier) is { } file) - Apply(file, identifier, EstFile.GetDefault(Manager, identifier.Slot, identifier.GenderRace, identifier.SetId)); - } - - public static bool Apply(EstFile file, EstIdentifier identifier, EstEntry entry) - => file.SetEntry(identifier.GenderRace, identifier.SetId, entry) switch - { - EstFile.EstEntryChange.Unchanged => false, - EstFile.EstEntryChange.Changed => true, - EstFile.EstEntryChange.Added => true, - EstFile.EstEntryChange.Removed => true, - _ => false, - }; + { } protected override void Dispose(bool _) - { - _estFaceFile?.Dispose(); - _estHairFile?.Dispose(); - _estBodyFile?.Dispose(); - _estHeadFile?.Dispose(); - _estFaceFile = null; - _estHairFile = null; - _estBodyFile = null; - _estHeadFile = null; - Clear(); - } - - private EstFile? GetCurrentFile(EstIdentifier identifier) - => identifier.Slot switch - { - EstType.Hair => _estHairFile, - EstType.Face => _estFaceFile, - EstType.Body => _estBodyFile, - EstType.Head => _estHeadFile, - _ => null, - }; - - private EstFile? GetFile(EstIdentifier identifier) - { - if (!Manager.CharacterUtility.Ready) - return null; - - return identifier.Slot switch - { - EstType.Hair => _estHairFile ??= new EstFile(Manager, EstType.Hair), - EstType.Face => _estFaceFile ??= new EstFile(Manager, EstType.Face), - EstType.Body => _estBodyFile ??= new EstFile(Manager, EstType.Body), - EstType.Head => _estHeadFile ??= new EstFile(Manager, EstType.Head), - _ => null, - }; - } + => Clear(); } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 0beb51d8..541b424d 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -1,6 +1,5 @@ using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; @@ -22,16 +21,6 @@ public sealed class GmpCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(GmpIdentifier identifier) { } - public static bool Apply(ExpandedGmpFile file, GmpIdentifier identifier, GmpEntry entry) - { - var origEntry = file[identifier.SetId]; - if (entry == origEntry) - return false; - - file[identifier.SetId] = entry; - return true; - } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index c8a116eb..014c7552 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -121,10 +121,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) switch (metaIndex) { case MetaIndex.Eqp: - Eqp.SetFiles(); break; case MetaIndex.Gmp: - Gmp.SetFiles(); break; case MetaIndex.HumanCmp: Rsp.SetFiles(); @@ -133,7 +131,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) case MetaIndex.HairEst: case MetaIndex.HeadEst: case MetaIndex.BodyEst: - Est.SetFile(metaIndex); break; default: Eqdp.SetFile(metaIndex); @@ -151,9 +148,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter TemporarilySetCmpFile() => Rsp.TemporarilySetFile(); - public MetaList.MetaReverter TemporarilySetEstFile(EstType type) - => Est.TemporarilySetFiles(type); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8701e3bb..dba971c6 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -103,8 +103,4 @@ public partial class ModCollection public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) => _cache?.Meta.TemporarilySetCmpFile() ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); - - public MetaList.MetaReverter TemporarilySetEstFile(CharacterUtility utility, EstType type) - => _cache?.Meta.TemporarilySetEstFile(type) - ?? utility.TemporarilyResetResource((MetaIndex)type); } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs new file mode 100644 index 00000000..3fab1434 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -0,0 +1,49 @@ +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public class EstHook : FastHook +{ + public delegate EstEntry Delegate(uint id, int estType, uint genderRace); + + private readonly MetaState _metaState; + + public EstHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, true); + } + + private EstEntry Detour(uint genderRace, int estType, uint id) + { + EstEntry ret; + if (_metaState.EstCollection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) + ret = entry.Entry; + else + ret = Task.Result.Original(genderRace, estType, id); + + Penumbra.Log.Information($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); + return ret; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static EstIdentifier Convert(uint genderRace, int estType, uint id) + { + var i = new PrimaryId((ushort)id); + var gr = (GenderRace)genderRace; + var type = estType switch + { + 1 => EstType.Face, + 2 => EstType.Hair, + 3 => EstType.Head, + 4 => EstType.Body, + _ => (EstType)0, + }; + return new EstIdentifier(i, type, gr); + } +} diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 9a68160b..6c9c1b7d 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -158,26 +158,26 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); } private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - using var est = GetEstChanges(drawObject, out var data); - return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + return ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); } private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) @@ -206,19 +206,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ResolvePath(drawObject, pathBuffer); } - private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data) - { - data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - if (_parent.InInternalResolve) - return DisposableContainer.Empty; - - return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Face), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Body), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Hair), - data.ModCollection.TemporarilySetEstFile(_parent.CharacterUtility, EstType.Head)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static Hook Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate { diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index c8ebe18f..8fa09232 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -18,7 +18,7 @@ namespace Penumbra.Interop.PathResolving; // GetSlotEqpData seems to be the only function using the EQP table. // It is only called by CheckSlotsForUnload (called by UpdateModels), // SetupModelAttributes (called by UpdateModels and OnModelLoadComplete) -// and a unnamed function called by UpdateRender. +// and an unnamed function called by UpdateRender. // It seems to be enough to change the EQP entries for UpdateModels. // GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables. @@ -35,7 +35,7 @@ namespace Penumbra.Interop.PathResolving; // they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create, // ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. -// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter. +// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor. public sealed unsafe class MetaState : IDisposable { private readonly Configuration _config; @@ -48,6 +48,7 @@ public sealed unsafe class MetaState : IDisposable public ResolveData CustomizeChangeCollection = ResolveData.Invalid; public ResolveData EqpCollection = ResolveData.Invalid; public ResolveData GmpCollection = ResolveData.Invalid; + public ResolveData EstCollection = ResolveData.Invalid; public PrimaryId UndividedGmpId = 0; private ResolveData _lastCreatedCollection = ResolveData.Invalid; From 9ecc4ab46d1b04e1bea0a6816d3e8ab06b862812 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Jun 2024 20:47:08 +0200 Subject: [PATCH 180/865] Remove CMP file. --- Penumbra/Collections/Cache/MetaCache.cs | 3 - Penumbra/Collections/Cache/RspCache.cs | 64 ++--------------- .../Collections/ModCollection.Cache.Access.cs | 4 -- .../Interop/Hooks/Meta/CalculateHeight.cs | 7 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 3 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 66 ++++++++++++++++++ Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 68 +++++++++++++++++++ .../Interop/Hooks/Meta/RspSetupCharacter.cs | 5 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 68 +++++++++++++++++++ Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 16 +++-- Penumbra/Interop/PathResolving/MetaState.cs | 9 ++- Penumbra/Meta/Files/CmpFile.cs | 8 +++ .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 2 +- 15 files changed, 244 insertions(+), 83 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/RspBustHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/RspHeightHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/RspTailHook.cs diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 014c7552..e6083351 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -145,9 +145,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) => Eqdp.TemporarilySetFile(genderRace, accessory); - public MetaList.MetaReverter TemporarilySetCmpFile() - => Rsp.TemporarilySetFile(); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs index 8a5fe97d..8a983c6c 100644 --- a/Penumbra/Collections/Cache/RspCache.cs +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -1,78 +1,26 @@ -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private CmpFile? _cmpFile; - public override void SetFiles() - => Manager.SetFile(_cmpFile, MetaIndex.HumanCmp); + { } protected override void IncorporateChangesInternal() - { - if (GetFile() is not { } file) - return; - - foreach (var (identifier, (_, entry)) in this) - Apply(file, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed RSP manipulations."); - } - - public MetaList.MetaReverter TemporarilySetFile() - => Manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp); + { } public void Reset() - { - if (_cmpFile == null) - return; - - _cmpFile.Reset(Keys.Select(identifier => (identifier.SubRace, identifier.Attribute))); - Clear(); - } + => Clear(); protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry) - { - if (GetFile() is { } file) - Apply(file, identifier, entry); - } + { } protected override void RevertModInternal(RspIdentifier identifier) - { - if (GetFile() is { } file) - Apply(file, identifier, CmpFile.GetDefault(Manager, identifier.SubRace, identifier.Attribute)); - } + { } - public static bool Apply(CmpFile file, RspIdentifier identifier, RspEntry entry) - { - var value = file[identifier.SubRace, identifier.Attribute]; - if (value == entry) - return false; - - file[identifier.SubRace, identifier.Attribute] = entry; - return true; - } protected override void Dispose(bool _) - { - _cmpFile?.Dispose(); - _cmpFile = null; - Clear(); - } - - private CmpFile? GetFile() - { - if (_cmpFile != null) - return _cmpFile; - - if (!Manager.CharacterUtility.Ready) - return null; - - return _cmpFile = new CmpFile(Manager); - } + => Clear(); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index dba971c6..d93a0f53 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -99,8 +99,4 @@ public partial class ModCollection var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; } - - public MetaList.MetaReverter TemporarilySetCmpFile(CharacterUtility utility) - => _cache?.Meta.TemporarilySetCmpFile() - ?? utility.TemporarilyResetResource(MetaIndex.HumanCmp); } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 2fd87f6e..5a207491 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.Interop.PathResolving; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -22,10 +23,10 @@ public sealed unsafe class CalculateHeight : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private ulong Detour(Character* character) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); - using var cmp = _metaState.ResolveRspData(collection.ModCollection); - var ret = Task.Result.Original.Invoke(character); + _metaState.RspCollection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var ret = Task.Result.Original.Invoke(character); Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + _metaState.RspCollection = ResolveData.Invalid; return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 2f717491..4e0a5744 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -25,12 +25,13 @@ public sealed unsafe class ChangeCustomize : FastHook private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment) { _metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - using var cmp = _metaState.ResolveRspData(_metaState.CustomizeChangeCollection.ModCollection); + _metaState.RspCollection = _metaState.CustomizeChangeCollection; using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true); using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); var ret = Task.Result.Original.Invoke(human, data, skipEquipment); Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); _metaState.CustomizeChangeCollection = ResolveData.Invalid; + _metaState.RspCollection = ResolveData.Invalid; return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 3fab1434..34935edb 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -27,7 +27,7 @@ public class EstHook : FastHook else ret = Task.Result.Original(genderRace, estType, id); - Penumbra.Log.Information($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); + Penumbra.Log.Excessive($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs new file mode 100644 index 00000000..fc1d743a --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -0,0 +1,66 @@ +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class RspBustHook : FastHook +{ + public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, + byte bustSize); + + private readonly MetaState _metaState; + private readonly MetaFileManager _metaFileManager; + + public RspBustHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) + { + _metaState = metaState; + _metaFileManager = metaFileManager; + Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, true); + } + + private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + { + if (gender == 0) + { + storage[0] = 1f; + storage[1] = 1f; + storage[2] = 1f; + return storage; + } + + var ret = storage; + if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + { + var bustScale = bustSize / 100f; + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); + var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); + storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); + storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); + storage[2] = GetValue(2, RspAttribute.BustMinZ, RspAttribute.BustMaxZ); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + float GetValue(int dimension, RspAttribute min, RspAttribute max) + { + var minValue = cache.Rsp.TryGetValue(new RspIdentifier(clan, min), out var minEntry) + ? minEntry.Entry.Value + : (ptr + dimension)->Value; + var maxValue = cache.Rsp.TryGetValue(new RspIdentifier(clan, max), out var maxEntry) + ? maxEntry.Entry.Value + : (ptr + 3 + dimension)->Value; + return (maxValue - minValue) * bustScale + minValue; + } + } + else + { + ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + } + + Penumbra.Log.Information( + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs new file mode 100644 index 00000000..883f5fc6 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -0,0 +1,68 @@ +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public class RspHeightHook : FastHook +{ + public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + + private readonly MetaState _metaState; + private readonly MetaFileManager _metaFileManager; + + public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) + { + _metaState = metaState; + _metaFileManager = metaFileManager; + Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, true); + } + + private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + { + float scale; + if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + { + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); + var (minIdent, maxIdent) = gender == 0 + ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) + : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); + + float minEntry, maxEntry; + if (cache.Rsp.TryGetValue(minIdent, out var min)) + { + minEntry = min.Entry.Value; + maxEntry = cache.Rsp.TryGetValue(maxIdent, out var max) + ? max.Entry.Value + : CmpFile.GetDefault(_metaFileManager, minIdent.SubRace, maxIdent.Attribute).Value; + } + else + { + var ptr = CmpFile.GetDefaults(_metaFileManager, minIdent.SubRace, minIdent.Attribute); + if (cache.Rsp.TryGetValue(maxIdent, out var max)) + { + minEntry = ptr->Value; + maxEntry = max.Entry.Value; + } + else + { + minEntry = ptr[0].Value; + maxEntry = ptr[1].Value; + } + } + + scale = (maxEntry - minEntry) * height / 100f + minEntry; + } + else + { + scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + } + + Penumbra.Log.Excessive( + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + return scale; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 8f8f1d78..831c99bb 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -30,8 +31,8 @@ public sealed unsafe class RspSetupCharacter : FastHook +{ + public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + + private readonly MetaState _metaState; + private readonly MetaFileManager _metaFileManager; + + public RspTailHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) + { + _metaState = metaState; + _metaFileManager = metaFileManager; + Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, true); + } + + private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) + { + float scale; + if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + { + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); + var (minIdent, maxIdent) = gender == 0 + ? (new RspIdentifier(clan, RspAttribute.MaleMinTail), new RspIdentifier(clan, RspAttribute.MaleMaxTail)) + : (new RspIdentifier(clan, RspAttribute.FemaleMinTail), new RspIdentifier(clan, RspAttribute.FemaleMaxTail)); + + float minEntry, maxEntry; + if (cache.Rsp.TryGetValue(minIdent, out var min)) + { + minEntry = min.Entry.Value; + maxEntry = cache.Rsp.TryGetValue(maxIdent, out var max) + ? max.Entry.Value + : CmpFile.GetDefault(_metaFileManager, minIdent.SubRace, maxIdent.Attribute).Value; + } + else + { + var ptr = CmpFile.GetDefaults(_metaFileManager, minIdent.SubRace, minIdent.Attribute); + if (cache.Rsp.TryGetValue(maxIdent, out var max)) + { + minEntry = ptr->Value; + maxEntry = max.Entry.Value; + } + else + { + minEntry = ptr[0].Value; + maxEntry = ptr[1].Value; + } + } + + scale = (maxEntry - minEntry) * tailLength / 100f + minEntry; + } + else + { + scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, tailLength); + } + + Penumbra.Log.Excessive( + $"[GetRspTail] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {tailLength}, returned {scale}."); + return scale; + } +} diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index a3e56d7f..8479968f 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -30,7 +30,7 @@ public sealed unsafe class SetupVisor : FastHook _metaState.GmpCollection = _collectionResolver.IdentifyCollection(drawObject, true); _metaState.UndividedGmpId = modelId; var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); - Penumbra.Log.Information($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); + Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); _metaState.GmpCollection = ResolveData.Invalid; return ret; } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 6c9c1b7d..17cfa3f6 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -159,25 +159,33 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection = ResolveData.Invalid; + return ret; } private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 8fa09232..3da94ce3 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -49,6 +49,7 @@ public sealed unsafe class MetaState : IDisposable public ResolveData EqpCollection = ResolveData.Invalid; public ResolveData GmpCollection = ResolveData.Invalid; public ResolveData EstCollection = ResolveData.Invalid; + public ResolveData RspCollection = ResolveData.Invalid; public PrimaryId UndividedGmpId = 0; private ResolveData _lastCreatedCollection = ResolveData.Invalid; @@ -96,9 +97,6 @@ public sealed unsafe class MetaState : IDisposable _ => DisposableContainer.Empty, }; - public MetaList.MetaReverter ResolveRspData(ModCollection collection) - => collection.TemporarilySetCmpFile(_characterUtility); - public DecalReverter ResolveDecal(ResolveData resolve, bool which) => new(_config, _characterUtility, _resources, resolve, which); @@ -132,9 +130,9 @@ public sealed unsafe class MetaState : IDisposable var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); - var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility); + RspCollection = _lastCreatedCollection; _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. - _characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp); + _characterBaseCreateMetaChanges = new DisposableContainer(decal); } private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject) @@ -144,6 +142,7 @@ public sealed unsafe class MetaState : IDisposable if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection, (nint)drawObject); + RspCollection = ResolveData.Invalid; _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 96cda496..8ca7cb80 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -46,6 +46,14 @@ public sealed unsafe class CmpFile : MetaBaseFile return *(RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); } + public static RspEntry* GetDefaults(MetaFileManager manager, SubRace subRace, RspAttribute attribute) + { + { + var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; + return (RspEntry*)(data + RacialScalingStart + ToRspIndex(subRace) * RspData.ByteSize + (int)attribute * 4); + } + } + private static int ToRspIndex(SubRace subRace) => subRace switch { diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 2b7904ce..be02e321 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -85,7 +85,7 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { using var dis = ImRaii.Disabled(disabled); ImGui.TableNextColumn(); - var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, defaultEntry.Value, entry.Value, out var newValue, + var ret = DragInput("##rspValue"u8, [], ImUtf8.GlobalScale * 150, entry.Value, defaultEntry.Value, out var newValue, RspEntry.MinValue, RspEntry.MaxValue, 0.001f, !disabled); if (ret) entry = new RspEntry(newValue); From 600fd2ecd36faa72292a7a8e2e8ce8982f6c708e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Jun 2024 01:02:42 +0200 Subject: [PATCH 181/865] Get rid off EQDP files --- Penumbra/Collections/Cache/EqdpCache.cs | 115 +++++------------- Penumbra/Collections/Cache/MetaCache.cs | 38 +----- .../Collections/ModCollection.Cache.Access.cs | 36 ------ .../Interop/Hooks/Meta/CalculateHeight.cs | 5 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 23 ++-- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 33 +++++ Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 32 +++++ Penumbra/Interop/Hooks/Meta/EqpHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 5 +- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 23 ++-- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 6 +- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 9 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 3 +- .../Interop/Hooks/Meta/RspSetupCharacter.cs | 5 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 6 +- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 9 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 45 ++++--- Penumbra/Interop/PathResolving/MetaState.cs | 33 ++--- Penumbra/Interop/Services/MetaList.cs | 2 +- Penumbra/Penumbra.cs | 2 - 23 files changed, 192 insertions(+), 246 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs create mode 100644 Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index c63403ae..5bfe2dbf 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,8 +1,5 @@ -using OtterGui; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -11,108 +8,60 @@ namespace Penumbra.Collections.Cache; public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly ExpandedEqdpFile?[] _eqdpFiles = new ExpandedEqdpFile[CharacterUtilityData.EqdpIndices.Length]; // TODO: female Hrothgar + private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), EqdpEntry> _fullEntries = []; public override void SetFiles() - { - for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) - Manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); - } + { } - public void SetFile(MetaIndex index) - { - var i = CharacterUtilityData.EqdpIndices.IndexOf(index); - if (i != -1) - Manager.SetFile(_eqdpFiles[i], index); - } - - public void ResetFiles() - { - foreach (var t in CharacterUtilityData.EqdpIndices) - Manager.SetFile(null, t); - } + public bool TryGetFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, out EqdpEntry entry) + => _fullEntries.TryGetValue((id, genderRace, accessory), out entry); protected override void IncorporateChangesInternal() - { - foreach (var (identifier, (_, entry)) in this) - Apply(GetFile(identifier)!, identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed EQDP manipulations."); - } - - public ExpandedEqdpFile? EqdpFile(GenderRace race, bool accessory) - => _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, CharacterUtilityData.EqdpIdx(race, accessory))]; // TODO: female Hrothgar - - public MetaList.MetaReverter? TemporarilySetFile(GenderRace genderRace, bool accessory) - { - var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); - if (idx < 0) - { - Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); - return null; - } - - var i = CharacterUtilityData.EqdpIndices.IndexOf(idx); - if (i < 0) - { - Penumbra.Log.Warning($"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}."); - return null; - } - - return Manager.TemporarilySetFile(_eqdpFiles[i], idx); - } + { } public void Reset() { - foreach (var file in _eqdpFiles.OfType()) - { - var relevant = CharacterUtility.RelevantIndices[file.Index.Value]; - file.Reset(Keys.Where(m => m.FileIndex() == relevant).Select(m => m.SetId)); - } - Clear(); + _fullEntries.Clear(); } protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry) { - if (GetFile(identifier) is { } file) - Apply(file, identifier, entry); + var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); + var mask = Eqdp.Mask(identifier.Slot); + if (!_fullEntries.TryGetValue(tuple, out var currentEntry)) + currentEntry = ExpandedEqdpFile.GetDefault(Manager, identifier); + + _fullEntries[tuple] = (currentEntry & ~mask) | (entry & mask); } protected override void RevertModInternal(EqdpIdentifier identifier) { - if (GetFile(identifier) is { } file) - Apply(file, identifier, ExpandedEqdpFile.GetDefault(Manager, identifier)); - } + var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); + var mask = Eqdp.Mask(identifier.Slot); - public static bool Apply(ExpandedEqdpFile file, EqdpIdentifier identifier, EqdpEntry entry) - { - var origEntry = file[identifier.SetId]; - var mask = Eqdp.Mask(identifier.Slot); - if ((origEntry & mask) == entry) - return false; - - file[identifier.SetId] = (origEntry & ~mask) | entry; - return true; + if (_fullEntries.TryGetValue(tuple, out var currentEntry)) + { + var def = ExpandedEqdpFile.GetDefault(Manager, identifier); + var newEntry = (currentEntry & ~mask) | (def & mask); + if (currentEntry != newEntry) + { + _fullEntries[tuple] = newEntry; + } + else + { + var slots = tuple.Item3 ? EquipSlotExtensions.AccessorySlots : EquipSlotExtensions.EquipmentSlots; + if (slots.All(s => !ContainsKey(identifier with { Slot = s }))) + _fullEntries.Remove(tuple); + else + _fullEntries[tuple] = newEntry; + } + } } protected override void Dispose(bool _) { - for (var i = 0; i < _eqdpFiles.Length; ++i) - { - _eqdpFiles[i]?.Dispose(); - _eqdpFiles[i] = null; - } - Clear(); - } - - private ExpandedEqdpFile? GetFile(EqdpIdentifier identifier) - { - if (!Manager.CharacterUtility.Ready) - return null; - - var index = Array.IndexOf(CharacterUtilityData.EqdpIndices, identifier.FileIndex()); - return _eqdpFiles[index] ??= new ExpandedEqdpFile(Manager, identifier.GenderRace, identifier.Slot.IsAccessory()); + _fullEntries.Clear(); } } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index e6083351..614a5a2c 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,7 +1,5 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -115,48 +113,18 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ~MetaCache() => Dispose(); - /// Set a single file. - public void SetFile(MetaIndex metaIndex) - { - switch (metaIndex) - { - case MetaIndex.Eqp: - break; - case MetaIndex.Gmp: - break; - case MetaIndex.HumanCmp: - Rsp.SetFiles(); - break; - case MetaIndex.FaceEst: - case MetaIndex.HairEst: - case MetaIndex.HeadEst: - case MetaIndex.BodyEst: - break; - default: - Eqdp.SetFile(metaIndex); - break; - } - } - /// Set the currently relevant IMC files for the collection cache. public void SetImcFiles(bool fromFullCompute) => Imc.SetFiles(fromFullCompute); - public MetaList.MetaReverter? TemporarilySetEqdpFile(GenderRace genderRace, bool accessory) - => Eqdp.TemporarilySetFile(genderRace, accessory); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) => Imc.GetFile(path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) - { - var eqdpFile = Eqdp.EqdpFile(race, accessory); - if (eqdpFile != null) - return primaryId.Id < eqdpFile.Count ? eqdpFile[primaryId] : default; - - return Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); - } + => Eqdp.TryGetFullEntry(primaryId, race, accessory, out var entry) + ? entry + : Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId) => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace)); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index d93a0f53..81751128 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,14 +1,9 @@ using OtterGui.Classes; -using Penumbra.GameData.Enums; using Penumbra.Mods; -using Penumbra.Interop.Structs; using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using Penumbra.Collections.Cache; -using Penumbra.Interop.Services; using Penumbra.Mods.Editor; -using Penumbra.GameData.Structs; namespace Penumbra.Collections; @@ -68,35 +63,4 @@ public partial class ModCollection internal SingleArray Conflicts(Mod mod) => _cache?.Conflicts(mod) ?? new SingleArray(); - - public void SetFiles(CharacterUtility utility) - { - if (_cache == null) - { - utility.ResetAll(); - } - else - { - _cache.Meta.SetFiles(); - Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Identifier}."); - } - } - - public void SetMetaFile(CharacterUtility utility, MetaIndex idx) - { - if (_cache == null) - utility.ResetResource(idx); - else - _cache.Meta.SetFile(idx); - } - - // Used for short periods of changed files. - public MetaList.MetaReverter? TemporarilySetEqdpFile(CharacterUtility utility, GenderRace genderRace, bool accessory) - { - if (_cache != null) - return _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory); - - var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); - return idx >= 0 ? utility.TemporarilyResetResource(idx) : null; - } } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 5a207491..7936b831 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -23,10 +23,11 @@ public sealed unsafe class CalculateHeight : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private ulong Detour(Character* character) { - _metaState.RspCollection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + _metaState.RspCollection.Push(collection); var ret = Task.Result.Original.Invoke(character); Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); - _metaState.RspCollection = ResolveData.Invalid; + _metaState.RspCollection.Pop(); return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index 4e0a5744..f589cf4e 100644 --- a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs +++ b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs @@ -1,12 +1,12 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.GameData.Structs; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - +using Penumbra.GameData; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + public sealed unsafe class ChangeCustomize : FastHook { private readonly CollectionResolver _collectionResolver; @@ -24,14 +24,15 @@ public sealed unsafe class ChangeCustomize : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment) { - _metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); - _metaState.RspCollection = _metaState.CustomizeChangeCollection; + var collection = _collectionResolver.IdentifyCollection((DrawObject*)human, true); + _metaState.CustomizeChangeCollection = collection; + _metaState.RspCollection.Push(collection); using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true); using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false); var ret = Task.Result.Original.Invoke(human, data, skipEquipment); Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}."); _metaState.CustomizeChangeCollection = ResolveData.Invalid; - _metaState.RspCollection = ResolveData.Invalid; + _metaState.RspCollection.Pop(); return ret; } -} +} diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs new file mode 100644 index 00000000..475e1eb7 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -0,0 +1,33 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class EqdpAccessoryHook : FastHook +{ + public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); + + private readonly MetaState _metaState; + + public EqdpAccessoryHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, true); + } + + private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) + { + if (_metaState.EqdpCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, out var newEntry)) + *entry = newEntry; + else + Task.Result.Original(utility, entry, setId, raceCode); + Penumbra.Log.Information( + $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs new file mode 100644 index 00000000..9b911710 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -0,0 +1,32 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class EqdpEquipHook : FastHook +{ + public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); + + private readonly MetaState _metaState; + + public EqdpEquipHook(HookManager hooks, MetaState metaState) + { + _metaState = metaState; + Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, true); + } + + private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) + { + if (_metaState.EqdpCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, out var newEntry)) + *entry = newEntry; + else + Task.Result.Original(utility, entry, setId, raceCode); + Penumbra.Log.Information( + $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); + } +} diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 448605c1..7107e26b 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -19,7 +19,7 @@ public unsafe class EqpHook : FastHook private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) { - if (_metaState.EqpCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (_metaState.EqpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { *flags = cache.Eqp.GetValues(armor); *flags = cache.GlobalEqp.Apply(*flags, armor); diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 34935edb..23931182 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -21,7 +21,7 @@ public class EstHook : FastHook private EstEntry Detour(uint genderRace, int estType, uint id) { EstEntry ret; - if (_metaState.EstCollection is { Valid: true, ModCollection.MetaCache: { } cache } + if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) ret = entry.Entry; else diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index beae6acc..a10b511a 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -29,8 +29,9 @@ public sealed unsafe class GetEqpIndirect : FastHook return; Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.EqpCollection.Push(collection); Task.Result.Original(drawObject); - _metaState.EqpCollection = ResolveData.Invalid; + _metaState.EqpCollection.Pop(); } } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 89aaa9b0..30ec2597 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,11 +1,11 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + public sealed unsafe class GetEqpIndirect2 : FastHook { private readonly CollectionResolver _collectionResolver; @@ -29,8 +29,9 @@ public sealed unsafe class GetEqpIndirect2 : FastHook return; Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}."); - _metaState.EqpCollection = _collectionResolver.IdentifyCollection(drawObject, true); + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.EqpCollection.Push(collection); Task.Result.Original(drawObject); - _metaState.EqpCollection = ResolveData.Invalid; + _metaState.EqpCollection.Pop(); } -} +} diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 60966fb7..256d8702 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -27,15 +27,15 @@ public unsafe class GmpHook : FastHook private nint Detour(nint gmpResource, uint dividedHeadId) { nint ret; - if (_metaState.GmpCollection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Gmp.TryGetValue(new GmpIdentifier(_metaState.UndividedGmpId), out var entry)) + if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } + && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) { if (entry.Entry.Enabled) { *StablePointer.Pointer = entry.Entry.Value; // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (_metaState.UndividedGmpId.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); + ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); } else { diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 10c12594..2c17362d 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -23,10 +23,11 @@ public sealed unsafe class ModelLoadComplete : FastHook } var ret = storage; - if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index 883f5fc6..cf88c34a 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; @@ -24,7 +25,7 @@ public class RspHeightHook : FastHook private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) { float scale; - if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 831c99bb..58856f52 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -31,8 +31,9 @@ public sealed unsafe class RspSetupCharacter : FastHook private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) { float scale; - if (bodyType < 2 && _metaState.RspCollection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index 8479968f..82b24dc4 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -27,11 +27,11 @@ public sealed unsafe class SetupVisor : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState) { - _metaState.GmpCollection = _collectionResolver.IdentifyCollection(drawObject, true); - _metaState.UndividedGmpId = modelId; + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.GmpCollection.Push((collection, modelId)); var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState); Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}."); - _metaState.GmpCollection = ResolveData.Invalid; + _metaState.GmpCollection.Pop(); return ret; } } diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index b0298ac7..76854bca 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -29,10 +29,11 @@ public sealed unsafe class UpdateModel : FastHook return; Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true); - _metaState.EqpCollection = collection; + var collection = _collectionResolver.IdentifyCollection(drawObject, true); + _metaState.EqpCollection.Push(collection); + _metaState.EqdpCollection.Push(collection); Task.Result.Original(drawObject); - _metaState.EqpCollection = ResolveData.Invalid; + _metaState.EqpCollection.Pop(); + _metaState.EqdpCollection.Pop(); } } diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 17cfa3f6..5941773f 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -1,11 +1,9 @@ using System.Text.Unicode; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Classes; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Interop.PathResolving; -using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Resources; @@ -149,42 +147,51 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveMdlHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) { - var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - using var eqdp = slotIndex > 9 || _parent.InInternalResolve - ? DisposableContainer.Empty - : _parent.MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), slotIndex < 5, slotIndex > 4); - return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + if (slotIndex < 10) + _parent.MetaState.EqdpCollection.Push(collection); + + var ret = ResolvePath(collection, _resolveMdlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex)); + if (slotIndex < 10) + _parent.MetaState.EqdpCollection.Pop(); + + return ret; } private nint ResolvePapHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, + _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + _parent.MetaState.EstCollection.Pop(); return ret; } private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); return ret; } private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); return ret; } private nint ResolveSkpHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { - _parent.MetaState.EstCollection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - var ret = ResolvePath(_parent.MetaState.EstCollection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); - _parent.MetaState.EstCollection = ResolveData.Invalid; + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveSkpPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); return ret; } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 3da94ce3..4bd23cf8 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -45,12 +45,14 @@ public sealed unsafe class MetaState : IDisposable private readonly CharacterUtility _characterUtility; private readonly CreateCharacterBase _createCharacterBase; - public ResolveData CustomizeChangeCollection = ResolveData.Invalid; - public ResolveData EqpCollection = ResolveData.Invalid; - public ResolveData GmpCollection = ResolveData.Invalid; - public ResolveData EstCollection = ResolveData.Invalid; - public ResolveData RspCollection = ResolveData.Invalid; - public PrimaryId UndividedGmpId = 0; + public ResolveData CustomizeChangeCollection = ResolveData.Invalid; + public readonly Stack EqpCollection = []; + public readonly Stack EqdpCollection = []; + public readonly Stack EstCollection = []; + public readonly Stack RspCollection = []; + + public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; + private ResolveData _lastCreatedCollection = ResolveData.Invalid; private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty; @@ -82,21 +84,6 @@ public sealed unsafe class MetaState : IDisposable return false; } - public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory) - => (equipment, accessory) switch - { - (true, true) => new DisposableContainer(race.Dependencies().SelectMany(r => new[] - { - collection.TemporarilySetEqdpFile(_characterUtility, r, false), - collection.TemporarilySetEqdpFile(_characterUtility, r, true), - })), - (true, false) => new DisposableContainer(race.Dependencies() - .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))), - (false, true) => new DisposableContainer(race.Dependencies() - .Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))), - _ => DisposableContainer.Empty, - }; - public DecalReverter ResolveDecal(ResolveData resolve, bool which) => new(_config, _characterUtility, _resources, resolve, which); @@ -130,7 +117,7 @@ public sealed unsafe class MetaState : IDisposable var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); - RspCollection = _lastCreatedCollection; + RspCollection.Push(_lastCreatedCollection); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. _characterBaseCreateMetaChanges = new DisposableContainer(decal); } @@ -142,7 +129,7 @@ public sealed unsafe class MetaState : IDisposable if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null) _communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection, (nint)drawObject); - RspCollection = ResolveData.Invalid; + RspCollection.Pop(); _lastCreatedCollection = ResolveData.Invalid; } diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index dc472b8e..24d3f088 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -87,7 +87,7 @@ public unsafe class MetaList : IDisposable => SetResourceInternal(_defaultResourceData, _defaultResourceSize); private void SetResourceToDefaultCollection() - => _utility.Active.Default.SetMetaFile(_utility, GlobalMetaIndex); + {} public void Dispose() { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 3bbfdf65..905b998d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -144,7 +144,6 @@ public class Penumbra : IDalamudPlugin { if (_characterUtility.Ready) { - _collectionManager.Active.Default.SetFiles(_characterUtility); _residentResources.Reload(); _redrawService.RedrawAll(RedrawType.Redraw); } @@ -153,7 +152,6 @@ public class Penumbra : IDalamudPlugin { if (_characterUtility.Ready) { - _characterUtility.ResetAll(); _residentResources.Reload(); _redrawService.RedrawAll(RedrawType.Redraw); } From 91d9e465ede9850fd7d5d0a457870e068cfd0cc8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Jun 2024 12:30:47 +0200 Subject: [PATCH 182/865] Improve eqdp. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EqdpCache.cs | 49 ++++++++----------- Penumbra/Collections/Cache/MetaCache.cs | 4 +- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 9 ++-- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 8 ++- Penumbra/Interop/PathResolving/MetaState.cs | 17 ------- .../Services/ShaderReplacementFixer.cs | 4 +- 7 files changed, 31 insertions(+), 62 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index cf1ff07e..3fbc7045 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit cf1ff07e900e2f93ab628a1fa535fc2b103794a5 +Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1 diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 5bfe2dbf..6047736b 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -1,20 +1,22 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), EqdpEntry> _fullEntries = []; + private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries = + []; public override void SetFiles() { } - public bool TryGetFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, out EqdpEntry entry) - => _fullEntries.TryGetValue((id, genderRace, accessory), out entry); + public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry) + => _fullEntries.TryGetValue((id, genderRace, accessory), out var pair) + ? (originalEntry & pair.InverseMask) | pair.Entry + : originalEntry; protected override void IncorporateChangesInternal() { } @@ -27,36 +29,27 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(EqdpIdentifier identifier, EqdpEntry entry) { - var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); - var mask = Eqdp.Mask(identifier.Slot); - if (!_fullEntries.TryGetValue(tuple, out var currentEntry)) - currentEntry = ExpandedEqdpFile.GetDefault(Manager, identifier); - - _fullEntries[tuple] = (currentEntry & ~mask) | (entry & mask); + var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); + var mask = Eqdp.Mask(identifier.Slot); + var inverseMask = ~mask; + if (_fullEntries.TryGetValue(tuple, out var pair)) + pair = ((pair.Entry & inverseMask) | (entry & mask), pair.InverseMask & inverseMask); + else + pair = (entry & mask, inverseMask); + _fullEntries[tuple] = pair; } protected override void RevertModInternal(EqdpIdentifier identifier) { var tuple = (identifier.SetId, identifier.GenderRace, identifier.Slot.IsAccessory()); - var mask = Eqdp.Mask(identifier.Slot); - if (_fullEntries.TryGetValue(tuple, out var currentEntry)) - { - var def = ExpandedEqdpFile.GetDefault(Manager, identifier); - var newEntry = (currentEntry & ~mask) | (def & mask); - if (currentEntry != newEntry) - { - _fullEntries[tuple] = newEntry; - } - else - { - var slots = tuple.Item3 ? EquipSlotExtensions.AccessorySlots : EquipSlotExtensions.EquipmentSlots; - if (slots.All(s => !ContainsKey(identifier with { Slot = s }))) - _fullEntries.Remove(tuple); - else - _fullEntries[tuple] = newEntry; - } - } + if (!_fullEntries.Remove(tuple, out var pair)) + return; + + var mask = Eqdp.Mask(identifier.Slot); + var newMask = pair.InverseMask | mask; + if (newMask is not EqdpEntry.FullMask) + _fullEntries[tuple] = (pair.Entry & ~mask, newMask); } protected override void Dispose(bool _) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 614a5a2c..92a445dd 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -122,9 +122,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) => Imc.GetFile(path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) - => Eqdp.TryGetFullEntry(primaryId, race, accessory, out var entry) - ? entry - : Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId); + => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId)); internal EstEntry GetEstEntry(EstType type, GenderRace genderRace, PrimaryId primaryId) => Est.GetEstEntry(new EstIdentifier(primaryId, type, genderRace)); diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index 475e1eb7..f7390ea3 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -3,7 +3,6 @@ using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; -using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; @@ -21,12 +20,10 @@ public unsafe class EqdpAccessoryHook : FastHook private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) { + Task.Result.Original(utility, entry, setId, raceCode); if (_metaState.EqdpCollection.TryPeek(out var collection) - && collection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, out var newEntry)) - *entry = newEntry; - else - Task.Result.Original(utility, entry, setId, raceCode); + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, *entry); Penumbra.Log.Information( $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 9b911710..9b635b1f 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -20,12 +20,10 @@ public unsafe class EqdpEquipHook : FastHook private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) { + Task.Result.Original(utility, entry, setId, raceCode); if (_metaState.EqdpCollection.TryPeek(out var collection) - && collection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Eqdp.TryGetFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, out var newEntry)) - *entry = newEntry; - else - Task.Result.Original(utility, entry, setId, raceCode); + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, *entry); Penumbra.Log.Information( $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 4bd23cf8..7f820b4e 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -2,13 +2,11 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using Penumbra.Collections; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; -using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using Penumbra.Interop.Hooks.Objects; @@ -87,21 +85,6 @@ public sealed unsafe class MetaState : IDisposable public DecalReverter ResolveDecal(ResolveData resolve, bool which) => new(_config, _characterUtility, _resources, resolve, which); - public static GenderRace GetHumanGenderRace(nint human) - => (GenderRace)((Human*)human)->RaceSexId; - - public static GenderRace GetDrawObjectGenderRace(nint drawObject) - { - var draw = (DrawObject*)drawObject; - if (draw->Object.GetObjectType() != ObjectType.CharacterBase) - return GenderRace.Unknown; - - var c = (CharacterBase*)drawObject; - return c->GetModelType() == CharacterBase.ModelType.Human - ? GetHumanGenderRace(drawObject) - : GenderRace.Unknown; - } - public void Dispose() { _createCharacterBase.Unsubscribe(OnCreatingCharacterBase); diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Services/ShaderReplacementFixer.cs index 3809ecbd..95e70b45 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Services/ShaderReplacementFixer.cs @@ -139,9 +139,9 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic // Performance considerations: // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; - // - Function is called each frame for each material on screen, after culling, i. e. up to thousands of times a frame in crowded areas ; + // - Function is called each frame for each material on screen, after culling, i.e. up to thousands of times a frame in crowded areas ; // - Swapping path is taken up to hundreds of times a frame. - // At the time of writing, the lock doesn't seem to have a noticeable impact in either framerate or CPU usage, but the swapping path shall still be avoided as much as possible. + // At the time of writing, the lock doesn't seem to have a noticeable impact in either frame rate or CPU usage, but the swapping path shall still be avoided as much as possible. lock (_skinLock) { try From be729afd4b382e618c91558373e013f34959ba02 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 16:39:10 +0200 Subject: [PATCH 183/865] Some cleanup --- Penumbra/Collections/Cache/ImcCache.cs | 2 -- Penumbra/Communication/CreatingCharacterBase.cs | 3 +-- Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 2 +- Penumbra/Mods/Manager/ModManager.cs | 2 +- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index a9daf795..c6bb0330 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,5 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using Penumbra.Collections.Manager; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta; diff --git a/Penumbra/Communication/CreatingCharacterBase.cs b/Penumbra/Communication/CreatingCharacterBase.cs index 8a906ca0..51d55868 100644 --- a/Penumbra/Communication/CreatingCharacterBase.cs +++ b/Penumbra/Communication/CreatingCharacterBase.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Services; @@ -19,7 +18,7 @@ public sealed class CreatingCharacterBase() { public enum Priority { - /// + /// Api = 0, /// diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index f7390ea3..aaaaccd4 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -24,7 +24,7 @@ public unsafe class EqdpAccessoryHook : FastHook if (_metaState.EqdpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, true, *entry); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 9b635b1f..2711f195 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -24,7 +24,7 @@ public unsafe class EqdpEquipHook : FastHook if (_metaState.EqdpCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) *entry = cache.Eqdp.ApplyFullEntry(new PrimaryId((ushort)setId), (GenderRace)raceCode, false, *entry); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index 13a5410d..86759460 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -59,7 +59,7 @@ public unsafe class RspBustHook : FastHook ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); } - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 62b54865..42082383 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -261,7 +261,7 @@ public sealed class ModManager : ModStorage, IDisposable /// /// Set the mod base directory. - /// If its not the first time, check if it is the same directory as before. + /// If it's not the first time, check if it is the same directory as before. /// Also checks if the directory is available and tries to create it if it is not. /// private void SetBaseDirectory(string newPath, bool firstTime, out string resultNewDir) From d7a8c9415bb57f8fe13e3cfeed067e6d4a579470 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jun 2024 23:11:42 +0200 Subject: [PATCH 184/865] Use specific counter for Imc. --- Penumbra/Collections/Cache/ImcCache.cs | 2 ++ Penumbra/Collections/ModCollection.cs | 2 ++ Penumbra/Interop/PathResolving/PathDataHandler.cs | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index c6bb0330..786463bc 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -68,6 +68,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { + ++Collection.ImcChangeCounter; if (Manager.CharacterUtility.Ready) ApplyFile(identifier, entry); } @@ -102,6 +103,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(ImcIdentifier identifier) { + ++Collection.ImcChangeCounter; var path = identifier.GamePath(); if (!_imcFiles.TryGetValue(path, out var pair)) return; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9286d459..eb5ab46a 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -56,6 +56,8 @@ public partial class ModCollection /// public int ChangeCounter { get; private set; } + public uint ImcChangeCounter { get; set; } + /// Increment the number of changes in the effective file list. public int IncrementCounter() => ++ChangeCounter; diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 5627e015..a8be97c8 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(ByteString path, ModCollection collection) - => CreateBase(path, collection); + => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] From 03d3c38ad577756d3a4fb20c1bd0417344538727 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 17:52:34 +0200 Subject: [PATCH 185/865] Improve Imc Handling. --- Penumbra/Collections/Cache/CollectionCache.cs | 16 +--- .../Cache/CollectionCacheManager.cs | 2 - Penumbra/Collections/Cache/ImcCache.cs | 58 ++++---------- Penumbra/Collections/Cache/MetaCache.cs | 8 +- .../Interop/PathResolving/PathResolver.cs | 50 +++--------- .../Interop/PathResolving/SubfileHelper.cs | 17 ++-- .../Interop/ResourceLoading/ResourceLoader.cs | 69 +++++++++++++++++ Penumbra/Interop/Services/CharacterUtility.cs | 35 +-------- Penumbra/Meta/Files/CmpFile.cs | 2 +- Penumbra/Meta/Files/EqdpFile.cs | 2 +- Penumbra/Meta/Files/EqpGmpFile.cs | 2 +- Penumbra/Meta/Files/EstFile.cs | 2 +- Penumbra/Meta/Files/EvpFile.cs | 6 +- Penumbra/Meta/Files/ImcFile.cs | 30 ++++---- Penumbra/Meta/Files/MetaBaseFile.cs | 77 +++++++++++++++---- Penumbra/Meta/MetaFileManager.cs | 54 +------------ 16 files changed, 197 insertions(+), 233 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index fd801d3b..4755840e 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -36,7 +36,7 @@ public sealed class CollectionCache : IDisposable => ConflictDict.Values; public SingleArray Conflicts(IMod mod) - => ConflictDict.TryGetValue(mod, out SingleArray c) ? c : new SingleArray(); + => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray(); private int _changedItemsSaveCounter = -1; @@ -125,12 +125,6 @@ public sealed class CollectionCache : IDisposable return ret; } - public void ForceFile(Utf8GamePath path, FullPath fullPath) - => _manager.AddChange(ChangeData.ForcedFile(this, path, fullPath)); - - public void RemovePath(Utf8GamePath path) - => _manager.AddChange(ChangeData.ForcedFile(this, path, FullPath.Empty)); - public void ReloadMod(IMod mod, bool addMetaChanges) => _manager.AddChange(ChangeData.ModReload(this, mod, addMetaChanges)); @@ -251,9 +245,6 @@ public sealed class CollectionCache : IDisposable if (addMetaChanges) { _collection.IncrementCounter(); - if (mod.TotalManipulations > 0) - AddMetaFiles(false); - _manager.MetaFileManager.ApplyDefaultFiles(_collection); } } @@ -408,11 +399,6 @@ public sealed class CollectionCache : IDisposable } - // Add all necessary meta file redirects. - public void AddMetaFiles(bool fromFullCompute) - => Meta.SetImcFiles(fromFullCompute); - - // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index ae424b94..02c9c8a9 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -180,8 +180,6 @@ public class CollectionCacheManager : IDisposable foreach (var mod in _modStorage) cache.AddModSync(mod, false); - cache.AddMetaFiles(true); - collection.IncrementCounter(); MetaFileManager.ApplyDefaultFiles(collection); diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 786463bc..e7eedc04 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -1,20 +1,22 @@ using Penumbra.GameData.Structs; -using Penumbra.Interop.PathResolving; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; +using Penumbra.String; namespace Penumbra.Collections.Cache; public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary)> _imcFiles = []; + private readonly Dictionary)> _imcFiles = []; public override void SetFiles() - => SetFiles(false); + { } - public bool GetFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) + public bool HasFile(ByteString path) + => _imcFiles.ContainsKey(path); + + public bool GetFile(ByteString path, [NotNullWhen(true)] out ImcFile? file) { if (!_imcFiles.TryGetValue(path, out var p)) { @@ -26,56 +28,31 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) return true; } - public void SetFiles(bool fromFullCompute) - { - if (fromFullCompute) - foreach (var (path, _) in _imcFiles) - Collection._cache!.ForceFileSync(path, PathDataHandler.CreateImc(path.Path, Collection)); - else - foreach (var (path, _) in _imcFiles) - Collection._cache!.ForceFile(path, PathDataHandler.CreateImc(path.Path, Collection)); - } - - public void ResetFiles() - { - foreach (var (path, _) in _imcFiles) - Collection._cache!.ForceFile(path, FullPath.Empty); - } - protected override void IncorporateChangesInternal() - { - if (!Manager.CharacterUtility.Ready) - return; - - foreach (var (identifier, (_, entry)) in this) - ApplyFile(identifier, entry); - - Penumbra.Log.Verbose($"{Collection.AnonymizedName}: Loaded {Count} delayed IMC manipulations."); - } + { } public void Reset() { - foreach (var (path, (file, set)) in _imcFiles) + foreach (var (_, (file, set)) in _imcFiles) { - Collection._cache!.RemovePath(path); file.Reset(); set.Clear(); } + _imcFiles.Clear(); Clear(); } protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { ++Collection.ImcChangeCounter; - if (Manager.CharacterUtility.Ready) - ApplyFile(identifier, entry); + ApplyFile(identifier, entry); } private void ApplyFile(ImcIdentifier identifier, ImcEntry entry) { - var path = identifier.GamePath(); + var path = identifier.GamePath().Path; try { if (!_imcFiles.TryGetValue(path, out var pair)) @@ -87,8 +64,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) pair.Item2.Add(identifier); _imcFiles[path] = pair; - var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); - Collection._cache!.ForceFile(path, fullPath); } catch (ImcException e) { @@ -104,7 +79,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(ImcIdentifier identifier) { ++Collection.ImcChangeCounter; - var path = identifier.GamePath(); + var path = identifier.GamePath().Path; if (!_imcFiles.TryGetValue(path, out var pair)) return; @@ -114,17 +89,12 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) if (pair.Item2.Count == 0) { _imcFiles.Remove(path); - Collection._cache!.ForceFile(pair.Item1.Path, FullPath.Empty); pair.Item1.Dispose(); return; } var def = ImcFile.GetDefault(Manager, pair.Item1.Path, identifier.EquipSlot, identifier.Variant, out _); - if (!Apply(pair.Item1, identifier, def)) - return; - - var fullPath = PathDataHandler.CreateImc(pair.Item1.Path.Path, Collection); - Collection._cache!.ForceFile(pair.Item1.Path, fullPath); + Apply(pair.Item1, identifier, def); } public static bool Apply(ImcFile file, ImcIdentifier identifier, ImcEntry entry) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 92a445dd..253f3c7f 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -36,7 +36,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Est.SetFiles(); Gmp.SetFiles(); Rsp.SetFiles(); - Imc.SetFiles(false); + Imc.SetFiles(); } public void Reset() @@ -113,13 +113,9 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ~MetaCache() => Dispose(); - /// Set the currently relevant IMC files for the collection cache. - public void SetImcFiles(bool fromFullCompute) - => Imc.SetFiles(fromFullCompute); - /// Try to obtain a manipulated IMC file. public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) - => Imc.GetFile(path, out file); + => Imc.GetFile(path.Path, out file); internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId)); diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index e5c75327..e069e3ea 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,11 +1,8 @@ -using System.Runtime; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.ResourceLoading; -using Penumbra.Interop.Structs; -using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Util; @@ -27,24 +24,16 @@ public class PathResolver : IDisposable public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) { - _performance = performance; - _config = config; - _collectionManager = collectionManager; - _subfileHelper = subfileHelper; - _pathState = pathState; - _metaState = metaState; - _gameState = gameState; - _collectionResolver = collectionResolver; - _loader = loader; - _loader.ResolvePath = ResolvePath; - _loader.FileLoaded += ImcLoadResource; - } - - /// Obtain a temporary or permanent collection by local ID. - public bool CollectionByLocalId(LocalCollectionId id, out ModCollection collection) - { - collection = _collectionManager.Storage.ByLocalId(id); - return collection != ModCollection.Empty; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _subfileHelper = subfileHelper; + _pathState = pathState; + _metaState = metaState; + _gameState = gameState; + _collectionResolver = collectionResolver; + _loader = loader; + _loader.ResolvePath = ResolvePath; } /// Try to resolve the given game path to the replaced path. @@ -120,7 +109,6 @@ public class PathResolver : IDisposable public unsafe void Dispose() { _loader.ResetResolvePath(); - _loader.FileLoaded -= ImcLoadResource; } /// Use the default method of path replacement. @@ -130,24 +118,6 @@ public class PathResolver : IDisposable return (resolved, _collectionManager.Active.Default.ToResolveData()); } - /// After loading an IMC file, replace its contents with the modded IMC file. - private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) - { - if (resource->FileType != ResourceType.Imc - || !PathDataHandler.Read(additionalData, out var data) - || data.Discriminator != PathDataHandler.Discriminator - || !Utf8GamePath.FromByteString(path, out var gamePath) - || !CollectionByLocalId(data.Collection, out var collection) - || !collection.HasCache - || !collection.GetImcFile(gamePath, out var file)) - return; - - file.Replace(resource); - Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } - /// Resolve a path from the interface collection. private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) => (_collectionManager.Active.Interface.ResolvePath(path), diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 793ea20b..b9631cf2 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -70,14 +70,15 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), - ResourceType.Avfx => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), - ResourceType.Tmb => PathDataHandler.CreateTmb(path, resolveData.ModCollection), - _ => resolved, - }; + resolved = type switch + { + ResourceType.Mtrl when nonDefault => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), + ResourceType.Avfx when nonDefault => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), + ResourceType.Tmb when nonDefault => PathDataHandler.CreateTmb(path, resolveData.ModCollection), + ResourceType.Imc when resolveData.ModCollection.MetaCache?.Imc.HasFile(path) ?? false => PathDataHandler.CreateImc(path, + resolveData.ModCollection), + _ => resolved, + }; data = (resolved, resolveData); } diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 7b49beab..fae38907 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,6 +1,9 @@ +using System.Collections.Frozen; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -10,6 +13,72 @@ using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; +public interface IFilePostProcessor : IService +{ + public ResourceType Type { get; } + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); +} + +public sealed class MaterialFilePostProcessor : IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Mtrl; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.ReadMtrl(additionalData, out var data)) + return; + } +} + +public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Imc; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) + return; + + var collection = collections.ByLocalId(data.Collection); + if (collection.MetaCache is not { } cache) + return; + + if (!cache.Imc.GetFile(originalGamePath, out var file)) + return; + + file.Replace(resource); + Penumbra.Log.Information( + $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + } +} + +public unsafe class FilePostProcessService : IRequiredService, IDisposable +{ + private readonly ResourceLoader _resourceLoader; + private readonly FrozenDictionary _processors; + + public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) + { + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.FileLoaded += OnFileLoaded; + } + + public void Dispose() + { + _resourceLoader.FileLoaded -= OnFileLoaded; + } + + private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ReadOnlySpan additionalData) + { + if (_processors.TryGetValue(resource->FileType, out var processor)) + processor.PostProcess(resource, path, additionalData); + } +} + public unsafe class ResourceLoader : IDisposable { private readonly ResourceService _resources; diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index da04bf90..9459df06 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.Interop.Structs; @@ -53,17 +52,15 @@ public unsafe class CharacterUtility : IDisposable public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; - private readonly IFramework _framework; - public readonly ActiveCollectionData Active; + private readonly IFramework _framework; - public CharacterUtility(IFramework framework, IGameInteropProvider interop, ActiveCollectionData active) + public CharacterUtility(IFramework framework, IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _lists = Enumerable.Range(0, RelevantIndices.Length) .Select(idx => new MetaList(this, new InternalIndex(idx))) .ToArray(); _framework = framework; - Active = active; LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); LoadDefaultResources(null!); if (!Ready) @@ -121,34 +118,6 @@ public unsafe class CharacterUtility : IDisposable LoadingFinished.Invoke(); } - public void SetResource(MetaIndex resourceIdx, nint data, int length) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - list.SetResource(data, length); - } - - public void ResetResource(MetaIndex resourceIdx) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - list.ResetResource(); - } - - public MetaList.MetaReverter TemporarilySetResource(MetaIndex resourceIdx, nint data, int length) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - return list.TemporarilySetResource(data, length); - } - - public MetaList.MetaReverter TemporarilyResetResource(MetaIndex resourceIdx) - { - var idx = ReverseIndices[(int)resourceIdx]; - var list = _lists[idx.Value]; - return list.TemporarilyResetResource(); - } - /// Return all relevant resources to the default resource. public void ResetAll() { diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 8ca7cb80..5028a3de 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -34,7 +34,7 @@ public sealed unsafe class CmpFile : MetaBaseFile } public CmpFile(MetaFileManager manager) - : base(manager, MetaIndex.HumanCmp) + : base(manager, manager.MarshalAllocator, MetaIndex.HumanCmp) { AllocateData(DefaultData.Length); Reset(); diff --git a/Penumbra/Meta/Files/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs index e46e82e9..34b4f25b 100644 --- a/Penumbra/Meta/Files/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -87,7 +87,7 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile } public ExpandedEqdpFile(MetaFileManager manager, GenderRace raceCode, bool accessory) - : base(manager, CharacterUtilityData.EqdpIdx(raceCode, accessory)) + : base(manager, manager.MarshalAllocator, CharacterUtilityData.EqdpIdx(raceCode, accessory)) { var def = (byte*)DefaultData.Data; var blockSize = *(ushort*)(def + IdentifierSize); diff --git a/Penumbra/Meta/Files/EqpGmpFile.cs b/Penumbra/Meta/Files/EqpGmpFile.cs index c47c84ef..a7540f4b 100644 --- a/Penumbra/Meta/Files/EqpGmpFile.cs +++ b/Penumbra/Meta/Files/EqpGmpFile.cs @@ -76,7 +76,7 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile } public ExpandedEqpGmpBase(MetaFileManager manager, bool gmp) - : base(manager, gmp ? MetaIndex.Gmp : MetaIndex.Eqp) + : base(manager, manager.MarshalAllocator, gmp ? MetaIndex.Gmp : MetaIndex.Eqp) { AllocateData(MaxSize); Reset(); diff --git a/Penumbra/Meta/Files/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs index f3860416..ba38d6d9 100644 --- a/Penumbra/Meta/Files/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -157,7 +157,7 @@ public sealed unsafe class EstFile : MetaBaseFile } public EstFile(MetaFileManager manager, EstType estType) - : base(manager, (MetaIndex)estType) + : base(manager, manager.MarshalAllocator, (MetaIndex)estType) { var length = DefaultData.Length; AllocateData(length + IncreaseSize); diff --git a/Penumbra/Meta/Files/EvpFile.cs b/Penumbra/Meta/Files/EvpFile.cs index 3d0b4dbe..6ab1591c 100644 --- a/Penumbra/Meta/Files/EvpFile.cs +++ b/Penumbra/Meta/Files/EvpFile.cs @@ -12,7 +12,7 @@ namespace Penumbra.Meta.Files; /// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. /// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect. /// -public unsafe class EvpFile : MetaBaseFile +public unsafe class EvpFile(MetaFileManager manager) : MetaBaseFile(manager, manager.MarshalAllocator, (MetaIndex)1) { public const int FlagArraySize = 512; @@ -57,8 +57,4 @@ public unsafe class EvpFile : MetaBaseFile return EvpFlag.None; } - - public EvpFile(MetaFileManager manager) - : base(manager, (MetaIndex)1) // TODO: Name - { } } diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 892f5b44..01ef3f16 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -7,16 +7,10 @@ using Penumbra.String.Functions; namespace Penumbra.Meta.Files; -public class ImcException : Exception +public class ImcException(ImcIdentifier identifier, Utf8GamePath path) : Exception { - public readonly ImcIdentifier Identifier; - public readonly string GamePath; - - public ImcException(ImcIdentifier identifier, Utf8GamePath path) - { - Identifier = identifier; - GamePath = path.ToString(); - } + public readonly ImcIdentifier Identifier = identifier; + public readonly string GamePath = path.ToString(); public override string Message => "Could not obtain default Imc File.\n" @@ -146,7 +140,11 @@ public unsafe class ImcFile : MetaBaseFile } public ImcFile(MetaFileManager manager, ImcIdentifier identifier) - : base(manager, 0) + : this(manager, manager.MarshalAllocator, identifier) + { } + + public ImcFile(MetaFileManager manager, IFileAllocator alloc, ImcIdentifier identifier) + : base(manager, alloc, 0) { var path = identifier.GamePathString(); Path = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; @@ -194,7 +192,13 @@ public unsafe class ImcFile : MetaBaseFile public void Replace(ResourceHandle* resource) { var (data, length) = resource->GetData(); - var newData = Manager.AllocateDefaultMemory(ActualLength, 8); + if (length == ActualLength) + { + MemoryUtility.MemCpyUnchecked((byte*)data, Data, ActualLength); + return; + } + + var newData = Manager.XivAllocator.Allocate(ActualLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); @@ -203,7 +207,7 @@ public unsafe class ImcFile : MetaBaseFile MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); - Manager.Free(data, length); - resource->SetData((IntPtr)newData, ActualLength); + Manager.XivAllocator.Release((void*)data, length); + resource->SetData((nint)newData, ActualLength); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index ab08efc2..86a55101 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -1,23 +1,75 @@ using Dalamud.Memory; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using OtterGui.Services; +using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String.Functions; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Meta.Files; -public unsafe class MetaBaseFile : IDisposable +public unsafe interface IFileAllocator { - protected readonly MetaFileManager Manager; + public T* Allocate(int length, int alignment = 1) where T : unmanaged; + public void Release(ref T* pointer, int length) where T : unmanaged; + + public void Release(void* pointer, int length) + { + var tmp = (byte*)pointer; + Release(ref tmp, length); + } + + public byte* Allocate(int length, int alignment = 1) + => Allocate(length, alignment); +} + +public sealed class MarshalAllocator : IFileAllocator +{ + public unsafe T* Allocate(int length, int alignment = 1) where T : unmanaged + => (T*)Marshal.AllocHGlobal(length * sizeof(T)); + + public unsafe void Release(ref T* pointer, int length) where T : unmanaged + { + Marshal.FreeHGlobal((nint)pointer); + pointer = null; + } +} + +public sealed unsafe class XivFileAllocator : IFileAllocator, IService +{ + /// + /// Allocate in the games space for file storage. + /// We only need this if using any meta file. + /// + [Signature(Sigs.GetFileSpace)] + private readonly nint _getFileSpaceAddress = nint.Zero; + + public XivFileAllocator(IGameInteropProvider provider) + => provider.InitializeFromAttributes(this); + + public IMemorySpace* GetFileSpace() + => ((delegate* unmanaged)_getFileSpaceAddress)(); + + public T* Allocate(int length, int alignment = 1) where T : unmanaged + => (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + + public void Release(ref T* pointer, int length) where T : unmanaged + { + IMemorySpace.Free(pointer, (ulong)(length * sizeof(T))); + pointer = null; + } +} + +public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, MetaIndex idx) : IDisposable +{ + protected readonly MetaFileManager Manager = manager; + protected readonly IFileAllocator Allocator = alloc; public byte* Data { get; private set; } public int Length { get; private set; } - public CharacterUtility.InternalIndex Index { get; } - - public MetaBaseFile(MetaFileManager manager, MetaIndex idx) - { - Manager = manager; - Index = CharacterUtility.ReverseIndices[(int)idx]; - } + public CharacterUtility.InternalIndex Index { get; } = CharacterUtility.ReverseIndices[(int)idx]; protected (IntPtr Data, int Length) DefaultData => Manager.CharacterUtility.DefaultResource(Index); @@ -30,7 +82,7 @@ public unsafe class MetaBaseFile : IDisposable protected void AllocateData(int length) { Length = length; - Data = (byte*)Manager.AllocateFileMemory(length); + Data = Allocator.Allocate(length); if (length > 0) GC.AddMemoryPressure(length); } @@ -38,8 +90,7 @@ public unsafe class MetaBaseFile : IDisposable /// Free memory. protected void ReleaseUnmanagedResources() { - var ptr = (IntPtr)Data; - MemoryHelper.GameFree(ref ptr, (ulong)Length); + Allocator.Release(Data, Length); if (Length > 0) GC.RemoveMemoryPressure(Length); @@ -53,7 +104,7 @@ public unsafe class MetaBaseFile : IDisposable if (newLength == Length) return; - var data = (byte*)Manager.AllocateFileMemory((ulong)newLength); + var data = Allocator.Allocate(newLength); if (newLength > Length) { MemoryUtility.MemCpyUnchecked(data, Data, Length); diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 40fceb07..81c0fa3e 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,14 +1,10 @@ using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.System.Memory; using OtterGui.Compression; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Interop.Services; -using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Mods; using Penumbra.Mods.Groups; @@ -28,6 +24,9 @@ public unsafe class MetaFileManager internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; internal readonly ImcChecker ImcChecker; + internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); + internal readonly IFileAllocator XivAllocator; + public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, @@ -42,6 +41,7 @@ public unsafe class MetaFileManager Identifier = identifier; Compactor = compactor; ImcChecker = new ImcChecker(this); + XivAllocator = new XivFileAllocator(interop); interop.InitializeFromAttributes(this); } @@ -76,57 +76,11 @@ public unsafe class MetaFileManager } } - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public void SetFile(MetaBaseFile? file, MetaIndex metaIndex) - { - if (file == null || !Config.EnableMods) - CharacterUtility.ResetResource(metaIndex); - else - CharacterUtility.SetResource(metaIndex, (nint)file.Data, file.Length); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex) - => Config.EnableMods - ? file == null - ? CharacterUtility.TemporarilyResetResource(metaIndex) - : CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length) - : MetaList.MetaReverter.Disabled; - public void ApplyDefaultFiles(ModCollection? collection) { if (ActiveCollections.Default != collection || !CharacterUtility.Ready || !Config.EnableMods) return; ResidentResources.Reload(); - if (collection._cache == null) - CharacterUtility.ResetAll(); - else - collection._cache.Meta.SetFiles(); } - - /// - /// Allocate in the games space for file storage. - /// We only need this if using any meta file. - /// - [Signature(Sigs.GetFileSpace)] - private readonly nint _getFileSpaceAddress = nint.Zero; - - public IMemorySpace* GetFileSpace() - => ((delegate* unmanaged)_getFileSpaceAddress)(); - - public void* AllocateFileMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateFileMemory(int length, int alignment = 0) - => AllocateFileMemory((ulong)length, (ulong)alignment); - - public void* AllocateDefaultMemory(ulong length, ulong alignment = 0) - => GetFileSpace()->Malloc(length, alignment); - - public void* AllocateDefaultMemory(int length, int alignment = 0) - => IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment); - - public void Free(nint ptr, int length) - => IMemorySpace.Free((void*)ptr, (ulong)length); } From f9c45a2f3f9bae991a67076622c6ef7d701c94ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 17:57:12 +0200 Subject: [PATCH 186/865] Clean unused functions. --- Penumbra/Collections/Cache/EqdpCache.cs | 10 ++----- Penumbra/Collections/Cache/EqpCache.cs | 12 --------- Penumbra/Collections/Cache/EstCache.cs | 12 --------- Penumbra/Collections/Cache/GmpCache.cs | 12 --------- Penumbra/Collections/Cache/IMetaCache.cs | 33 +++++------------------- Penumbra/Collections/Cache/ImcCache.cs | 7 ----- Penumbra/Collections/Cache/MetaCache.cs | 10 ------- Penumbra/Collections/Cache/RspCache.cs | 13 ---------- 8 files changed, 9 insertions(+), 100 deletions(-) diff --git a/Penumbra/Collections/Cache/EqdpCache.cs b/Penumbra/Collections/Cache/EqdpCache.cs index 6047736b..5e0626cf 100644 --- a/Penumbra/Collections/Cache/EqdpCache.cs +++ b/Penumbra/Collections/Cache/EqdpCache.cs @@ -10,17 +10,11 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) private readonly Dictionary<(PrimaryId Id, GenderRace GenderRace, bool Accessory), (EqdpEntry Entry, EqdpEntry InverseMask)> _fullEntries = []; - public override void SetFiles() - { } - public EqdpEntry ApplyFullEntry(PrimaryId id, GenderRace genderRace, bool accessory, EqdpEntry originalEntry) => _fullEntries.TryGetValue((id, genderRace, accessory), out var pair) ? (originalEntry & pair.InverseMask) | pair.Entry : originalEntry; - protected override void IncorporateChangesInternal() - { } - public void Reset() { Clear(); @@ -46,8 +40,8 @@ public sealed class EqdpCache(MetaFileManager manager, ModCollection collection) if (!_fullEntries.Remove(tuple, out var pair)) return; - var mask = Eqdp.Mask(identifier.Slot); - var newMask = pair.InverseMask | mask; + var mask = Eqdp.Mask(identifier.Slot); + var newMask = pair.InverseMask | mask; if (newMask is not EqdpEntry.FullMask) _fullEntries[tuple] = (pair.Entry & ~mask, newMask); } diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 7ba0c489..60e38aef 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -8,12 +8,6 @@ namespace Penumbra.Collections.Cache; public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public unsafe EqpEntry GetValues(CharacterArmor* armor) => GetSingleValue(armor[0].Set, EquipSlot.Head) | GetSingleValue(armor[1].Set, EquipSlot.Body) @@ -28,12 +22,6 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) public void Reset() => Clear(); - protected override void ApplyModInternal(EqpIdentifier identifier, EqpEntry entry) - { } - - protected override void RevertModInternal(EqpIdentifier identifier) - { } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/EstCache.cs b/Penumbra/Collections/Cache/EstCache.cs index ff94324e..aff8beef 100644 --- a/Penumbra/Collections/Cache/EstCache.cs +++ b/Penumbra/Collections/Cache/EstCache.cs @@ -6,12 +6,6 @@ namespace Penumbra.Collections.Cache; public sealed class EstCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public EstEntry GetEstEntry(EstIdentifier identifier) => TryGetValue(identifier, out var entry) ? entry.Entry @@ -20,12 +14,6 @@ public sealed class EstCache(MetaFileManager manager, ModCollection collection) public void Reset() => Clear(); - protected override void ApplyModInternal(EstIdentifier identifier, EstEntry entry) - { } - - protected override void RevertModInternal(EstIdentifier identifier) - { } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/GmpCache.cs b/Penumbra/Collections/Cache/GmpCache.cs index 541b424d..9170b871 100644 --- a/Penumbra/Collections/Cache/GmpCache.cs +++ b/Penumbra/Collections/Cache/GmpCache.cs @@ -6,21 +6,9 @@ namespace Penumbra.Collections.Cache; public sealed class GmpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public void Reset() => Clear(); - protected override void ApplyModInternal(GmpIdentifier identifier, GmpEntry entry) - { } - - protected override void RevertModInternal(GmpIdentifier identifier) - { } - protected override void Dispose(bool _) => Clear(); } diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/IMetaCache.cs index dd218b48..fecc6f50 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/IMetaCache.cs @@ -4,30 +4,19 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; -public abstract class MetaCacheBase +public abstract class MetaCacheBase(MetaFileManager manager, ModCollection collection) : Dictionary where TIdentifier : unmanaged, IMetaIdentifier where TEntry : unmanaged { - protected MetaCacheBase(MetaFileManager manager, ModCollection collection) - { - Manager = manager; - Collection = collection; - if (!Manager.CharacterUtility.Ready) - Manager.CharacterUtility.LoadingFinished += IncorporateChanges; - } - - protected readonly MetaFileManager Manager; - protected readonly ModCollection Collection; + protected readonly MetaFileManager Manager = manager; + protected readonly ModCollection Collection = collection; public void Dispose() { - Manager.CharacterUtility.LoadingFinished -= IncorporateChanges; Dispose(true); } - public abstract void SetFiles(); - public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) { lock (this) @@ -59,20 +48,12 @@ public abstract class MetaCacheBase return true; } - private void IncorporateChanges() - { - lock (this) - { - IncorporateChangesInternal(); - } - if (Manager.ActiveCollections.Default == Collection && Manager.Config.EnableMods) - SetFiles(); - } + protected virtual void ApplyModInternal(TIdentifier identifier, TEntry entry) + { } - protected abstract void ApplyModInternal(TIdentifier identifier, TEntry entry); - protected abstract void RevertModInternal(TIdentifier identifier); - protected abstract void IncorporateChangesInternal(); + protected virtual void RevertModInternal(TIdentifier identifier) + { } protected virtual void Dispose(bool _) { } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index e7eedc04..40c3d2c7 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -10,9 +10,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) { private readonly Dictionary)> _imcFiles = []; - public override void SetFiles() - { } - public bool HasFile(ByteString path) => _imcFiles.ContainsKey(path); @@ -28,10 +25,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) return true; } - protected override void IncorporateChangesInternal() - { } - - public void Reset() { foreach (var (_, (file, set)) in _imcFiles) diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 253f3c7f..02056fad 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -29,16 +29,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); - public void SetFiles() - { - Eqp.SetFiles(); - Eqdp.SetFiles(); - Est.SetFiles(); - Gmp.SetFiles(); - Rsp.SetFiles(); - Imc.SetFiles(); - } - public void Reset() { Eqp.Reset(); diff --git a/Penumbra/Collections/Cache/RspCache.cs b/Penumbra/Collections/Cache/RspCache.cs index 8a983c6c..064b1f44 100644 --- a/Penumbra/Collections/Cache/RspCache.cs +++ b/Penumbra/Collections/Cache/RspCache.cs @@ -5,22 +5,9 @@ namespace Penumbra.Collections.Cache; public sealed class RspCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public override void SetFiles() - { } - - protected override void IncorporateChangesInternal() - { } - public void Reset() => Clear(); - protected override void ApplyModInternal(RspIdentifier identifier, RspEntry entry) - { } - - protected override void RevertModInternal(RspIdentifier identifier) - { } - - protected override void Dispose(bool _) => Clear(); } From cf1dcfcb7cb27e6928810b75c63f1f352da4ecd2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 18:33:37 +0200 Subject: [PATCH 187/865] Improve Path preprocessing. --- .../Interop/PathResolving/PathResolver.cs | 23 ++-- .../Interop/PathResolving/SubfileHelper.cs | 16 --- .../Processing/AvfxPathPreProcessor.cs | 16 +++ .../Processing/FilePostProcessService.cs | 39 ++++++ .../Processing/GamePathPreProcessService.cs | 37 +++++ .../Processing/ImcFilePostProcessor.cs | 30 ++++ .../Interop/Processing/ImcPathPreProcessor.cs | 18 +++ .../Processing/MaterialFilePostProcessor.cs | 18 +++ .../Processing/MtrlPathPreProcessor.cs | 16 +++ .../Interop/Processing/TmbPathPreProcessor.cs | 16 +++ .../Interop/ResourceLoading/ResourceLoader.cs | 69 ---------- Penumbra/Interop/Services/CharacterUtility.cs | 8 +- Penumbra/Interop/Services/MetaList.cs | 130 +----------------- Penumbra/UI/Tabs/Debug/DebugTab.cs | 19 --- 14 files changed, 209 insertions(+), 246 deletions(-) create mode 100644 Penumbra/Interop/Processing/AvfxPathPreProcessor.cs create mode 100644 Penumbra/Interop/Processing/FilePostProcessService.cs create mode 100644 Penumbra/Interop/Processing/GamePathPreProcessService.cs create mode 100644 Penumbra/Interop/Processing/ImcFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/ImcPathPreProcessor.cs create mode 100644 Penumbra/Interop/Processing/MaterialFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/MtrlPathPreProcessor.cs create mode 100644 Penumbra/Interop/Processing/TmbPathPreProcessor.cs diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index e069e3ea..f31b3323 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Interop.Processing; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; using Penumbra.Util; @@ -15,14 +16,16 @@ public class PathResolver : IDisposable private readonly CollectionManager _collectionManager; private readonly ResourceLoader _loader; - private readonly SubfileHelper _subfileHelper; - private readonly PathState _pathState; - private readonly MetaState _metaState; - private readonly GameState _gameState; - private readonly CollectionResolver _collectionResolver; + private readonly SubfileHelper _subfileHelper; + private readonly PathState _pathState; + private readonly MetaState _metaState; + private readonly GameState _gameState; + private readonly CollectionResolver _collectionResolver; + private readonly GamePathPreProcessService _preprocessor; - public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, - SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState) + public PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ResourceLoader loader, + SubfileHelper subfileHelper, PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState, + GamePathPreProcessService preprocessor) { _performance = performance; _config = config; @@ -31,6 +34,7 @@ public class PathResolver : IDisposable _pathState = pathState; _metaState = metaState; _gameState = gameState; + _preprocessor = preprocessor; _collectionResolver = collectionResolver; _loader = loader; _loader.ResolvePath = ResolvePath; @@ -102,11 +106,10 @@ public class PathResolver : IDisposable // so that the functions loading tex and shpk can find that path and use its collection. // We also need to handle defaulted materials against a non-default collection. var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; - SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, gamePath, out var pair); - return pair; + return _preprocessor.PreProcess(resolveData, path, nonDefault, type, resolved, gamePath); } - public unsafe void Dispose() + public void Dispose() { _loader.ResetResolvePath(); } diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index b9631cf2..3cefd98d 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -66,22 +66,6 @@ public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection Materials, TMB, and AVFX need to be set per collection, so they can load their sub files independently of each other. - public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved, - Utf8GamePath originalPath, out (FullPath?, ResolveData) data) - { - resolved = type switch - { - ResourceType.Mtrl when nonDefault => PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalPath), - ResourceType.Avfx when nonDefault => PathDataHandler.CreateAvfx(path, resolveData.ModCollection), - ResourceType.Tmb when nonDefault => PathDataHandler.CreateTmb(path, resolveData.ModCollection), - ResourceType.Imc when resolveData.ModCollection.MetaCache?.Imc.HasFile(path) ?? false => PathDataHandler.CreateImc(path, - resolveData.ModCollection), - _ => resolved, - }; - data = (resolved, resolveData); - } - public void Dispose() { _loader.ResourceLoaded -= SubfileContainerRequested; diff --git a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs new file mode 100644 index 00000000..56f693e6 --- /dev/null +++ b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs @@ -0,0 +1,16 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class AvfxPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Avfx; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + => nonDefault ? PathDataHandler.CreateAvfx(path, resolveData.ModCollection) : resolved; +} diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs new file mode 100644 index 00000000..0dc62b3d --- /dev/null +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -0,0 +1,39 @@ +using System.Collections.Frozen; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public interface IFilePostProcessor : IService +{ + public ResourceType Type { get; } + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); +} + +public unsafe class FilePostProcessService : IRequiredService, IDisposable +{ + private readonly ResourceLoader _resourceLoader; + private readonly FrozenDictionary _processors; + + public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) + { + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.FileLoaded += OnFileLoaded; + } + + public void Dispose() + { + _resourceLoader.FileLoaded -= OnFileLoaded; + } + + private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + ReadOnlySpan additionalData) + { + if (_processors.TryGetValue(resource->FileType, out var processor)) + processor.PostProcess(resource, path, additionalData); + } +} diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs new file mode 100644 index 00000000..004b7168 --- /dev/null +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -0,0 +1,37 @@ +using System.Collections.Frozen; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public interface IPathPreProcessor : IService +{ + public ResourceType Type { get; } + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved); +} + +public class GamePathPreProcessService : IService +{ + private readonly FrozenDictionary _processors; + + public GamePathPreProcessService(ServiceManager services) + { + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + } + + + public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, + FullPath? resolved, + Utf8GamePath originalPath) + { + if (!_processors.TryGetValue(type, out var processor)) + return (resolved, resolveData); + + resolved = processor.PreProcess(resolveData, path, originalPath, nonDefault, resolved); + return (resolved, resolveData); + } +} diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs new file mode 100644 index 00000000..4a0ebe22 --- /dev/null +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -0,0 +1,30 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Imc; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) + return; + + var collection = collections.ByLocalId(data.Collection); + if (collection.MetaCache is not { } cache) + return; + + if (!cache.Imc.GetFile(originalGamePath, out var file)) + return; + + file.Replace(resource); + Penumbra.Log.Information( + $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + } +} diff --git a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs new file mode 100644 index 00000000..907d7587 --- /dev/null +++ b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs @@ -0,0 +1,18 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class ImcPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Imc; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved) + => resolveData.ModCollection.MetaCache?.Imc.HasFile(originalGamePath.Path) ?? false + ? PathDataHandler.CreateImc(path, resolveData.ModCollection) + : resolved; +} diff --git a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs new file mode 100644 index 00000000..02b5d46c --- /dev/null +++ b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs @@ -0,0 +1,18 @@ +using Penumbra.Api.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class MaterialFilePostProcessor //: IFilePostProcessor +{ + public ResourceType Type + => ResourceType.Mtrl; + + public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.ReadMtrl(additionalData, out var data)) + return; + } +} diff --git a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs new file mode 100644 index 00000000..8fb2400b --- /dev/null +++ b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs @@ -0,0 +1,16 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class MtrlPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Mtrl; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + => nonDefault ? PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalGamePath) : resolved; +} diff --git a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs new file mode 100644 index 00000000..dd887819 --- /dev/null +++ b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs @@ -0,0 +1,16 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class TmbPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Tmb; + + public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + => nonDefault ? PathDataHandler.CreateTmb(path, resolveData.ModCollection) : resolved; +} diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index fae38907..7b49beab 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,9 +1,6 @@ -using System.Collections.Frozen; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -13,72 +10,6 @@ using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; -public interface IFilePostProcessor : IService -{ - public ResourceType Type { get; } - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); -} - -public sealed class MaterialFilePostProcessor : IFilePostProcessor -{ - public ResourceType Type - => ResourceType.Mtrl; - - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) - { - if (!PathDataHandler.ReadMtrl(additionalData, out var data)) - return; - } -} - -public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFilePostProcessor -{ - public ResourceType Type - => ResourceType.Imc; - - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) - { - if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) - return; - - var collection = collections.ByLocalId(data.Collection); - if (collection.MetaCache is not { } cache) - return; - - if (!cache.Imc.GetFile(originalGamePath, out var file)) - return; - - file.Replace(resource); - Penumbra.Log.Information( - $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); - } -} - -public unsafe class FilePostProcessService : IRequiredService, IDisposable -{ - private readonly ResourceLoader _resourceLoader; - private readonly FrozenDictionary _processors; - - public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) - { - _resourceLoader = resourceLoader; - _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.FileLoaded += OnFileLoaded; - } - - public void Dispose() - { - _resourceLoader.FileLoaded -= OnFileLoaded; - } - - private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) - { - if (_processors.TryGetValue(resource->FileType, out var processor)) - processor.PostProcess(resource, path, additionalData); - } -} - public unsafe class ResourceLoader : IDisposable { private readonly ResourceService _resources; diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 9459df06..0877d221 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -46,9 +46,6 @@ public unsafe class CharacterUtility : IDisposable private readonly MetaList[] _lists; - public IReadOnlyList Lists - => _lists; - public (nint Address, int Size) DefaultResource(InternalIndex idx) => _lists[idx.Value].DefaultResource; @@ -58,7 +55,7 @@ public unsafe class CharacterUtility : IDisposable { interop.InitializeFromAttributes(this); _lists = Enumerable.Range(0, RelevantIndices.Length) - .Select(idx => new MetaList(this, new InternalIndex(idx))) + .Select(idx => new MetaList(new InternalIndex(idx))) .ToArray(); _framework = framework; LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); @@ -124,9 +121,6 @@ public unsafe class CharacterUtility : IDisposable if (!Ready) return; - foreach (var list in _lists) - list.Dispose(); - Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; diff --git a/Penumbra/Interop/Services/MetaList.cs b/Penumbra/Interop/Services/MetaList.cs index 24d3f088..839c289e 100644 --- a/Penumbra/Interop/Services/MetaList.cs +++ b/Penumbra/Interop/Services/MetaList.cs @@ -2,26 +2,14 @@ using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; -public unsafe class MetaList : IDisposable +public class MetaList(CharacterUtility.InternalIndex index) { - private readonly CharacterUtility _utility; - private readonly LinkedList _entries = new(); - public readonly CharacterUtility.InternalIndex Index; - public readonly MetaIndex GlobalMetaIndex; - - public IReadOnlyCollection Entries - => _entries; + public readonly CharacterUtility.InternalIndex Index = index; + public readonly MetaIndex GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value]; private nint _defaultResourceData = nint.Zero; - private int _defaultResourceSize = 0; - public bool Ready { get; private set; } = false; - - public MetaList(CharacterUtility utility, CharacterUtility.InternalIndex index) - { - _utility = utility; - Index = index; - GlobalMetaIndex = CharacterUtility.RelevantIndices[index.Value]; - } + private int _defaultResourceSize; + public bool Ready { get; private set; } public void SetDefaultResource(nint data, int size) { @@ -31,116 +19,8 @@ public unsafe class MetaList : IDisposable _defaultResourceData = data; _defaultResourceSize = size; Ready = _defaultResourceData != nint.Zero && size != 0; - if (_entries.Count <= 0) - return; - - var first = _entries.First!.Value; - SetResource(first.Data, first.Length); } public (nint Address, int Size) DefaultResource => (_defaultResourceData, _defaultResourceSize); - - public MetaReverter TemporarilySetResource(nint data, int length) - { - Penumbra.Log.Excessive($"Temporarily set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); - var reverter = new MetaReverter(this, data, length); - _entries.AddFirst(reverter); - SetResourceInternal(data, length); - return reverter; - } - - public MetaReverter TemporarilyResetResource() - { - Penumbra.Log.Excessive( - $"Temporarily reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); - var reverter = new MetaReverter(this); - _entries.AddFirst(reverter); - ResetResourceInternal(); - return reverter; - } - - public void SetResource(nint data, int length) - { - Penumbra.Log.Excessive($"Set resource {GlobalMetaIndex} to 0x{(ulong)data:X} ({length} bytes)."); - SetResourceInternal(data, length); - } - - public void ResetResource() - { - Penumbra.Log.Excessive($"Reset resource {GlobalMetaIndex} to default at 0x{_defaultResourceData:X} ({_defaultResourceSize} bytes)."); - ResetResourceInternal(); - } - - /// Set the currently stored data of this resource to new values. - private void SetResourceInternal(nint data, int length) - { - if (!Ready) - return; - - var resource = _utility.Address->Resource(GlobalMetaIndex); - resource->SetData(data, length); - } - - /// Reset the currently stored data of this resource to its default values. - private void ResetResourceInternal() - => SetResourceInternal(_defaultResourceData, _defaultResourceSize); - - private void SetResourceToDefaultCollection() - {} - - public void Dispose() - { - if (_entries.Count > 0) - { - foreach (var entry in _entries) - entry.Disposed = true; - - _entries.Clear(); - } - - ResetResourceInternal(); - } - - public sealed class MetaReverter(MetaList metaList, nint data, int length) : IDisposable - { - public static readonly MetaReverter Disabled = new(null!) { Disposed = true }; - - public readonly MetaList MetaList = metaList; - public readonly nint Data = data; - public readonly int Length = length; - public readonly bool Resetter; - public bool Disposed; - - public MetaReverter(MetaList metaList) - : this(metaList, nint.Zero, 0) - => Resetter = true; - - public void Dispose() - { - if (Disposed) - return; - - var list = MetaList._entries; - var wasCurrent = ReferenceEquals(this, list.First?.Value); - list.Remove(this); - if (!wasCurrent) - return; - - if (list.Count == 0) - { - MetaList.SetResourceToDefaultCollection(); - } - else - { - var next = list.First!.Value; - if (next.Resetter) - MetaList.ResetResourceInternal(); - else - MetaList.SetResourceInternal(next.Data, next.Length); - } - - Disposed = true; - } - } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 396029d5..1519ebf0 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -179,8 +179,6 @@ public class DebugTab : Window, ITab ImGui.NewLine(); DrawData(); ImGui.NewLine(); - DrawDebugTabMetaLists(); - ImGui.NewLine(); DrawResourceProblems(); ImGui.NewLine(); DrawPlayerModelInfo(); @@ -788,23 +786,6 @@ public class DebugTab : Window, ITab } } - private void DrawDebugTabMetaLists() - { - if (!ImGui.CollapsingHeader("Metadata Changes")) - return; - - using var table = Table("##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - foreach (var list in _characterUtility.Lists) - { - ImGuiUtil.DrawTableColumn(list.GlobalMetaIndex.ToString()); - ImGuiUtil.DrawTableColumn(list.Entries.Count.ToString()); - ImGuiUtil.DrawTableColumn(string.Join(", ", list.Entries.Select(e => $"0x{e.Data:X}"))); - } - } - /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() { From e05dbe988570d949f6af81f65b3073c875fb9fb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Jun 2024 21:59:04 +0200 Subject: [PATCH 188/865] Make everything services. --- OtterGui | 2 +- Penumbra/Api/TempModManager.cs | 3 +- .../Cache/CollectionCacheManager.cs | 3 +- .../Collections/Manager/ActiveCollections.cs | 5 +- .../Collections/Manager/CollectionEditor.cs | 3 +- .../Collections/Manager/CollectionManager.cs | 3 +- .../Collections/Manager/CollectionStorage.cs | 5 +- .../Collections/Manager/InheritanceManager.cs | 14 +- .../Manager/TempCollectionManager.cs | 3 +- Penumbra/CommandHandler.cs | 4 +- Penumbra/Configuration.cs | 3 +- Penumbra/EphemeralConfig.cs | 3 +- Penumbra/Import/Models/ModelManager.cs | 4 +- Penumbra/Import/Textures/TextureManager.cs | 26 ++- .../Interop/PathResolving/CutsceneService.cs | 3 +- .../IdentifiedCollectionCache.cs | 4 +- Penumbra/Interop/PathResolving/MetaState.cs | 3 +- .../Interop/PathResolving/PathResolver.cs | 3 +- Penumbra/Interop/PathResolving/PathState.cs | 3 +- .../Interop/PathResolving/SubfileHelper.cs | 4 +- .../ResourceLoading/FileReadService.cs | 3 +- .../Interop/ResourceLoading/ResourceLoader.cs | 5 +- .../ResourceLoading/ResourceManagerService.cs | 3 +- .../ResourceLoading/ResourceService.cs | 3 +- .../Interop/ResourceLoading/TexMdlService.cs | 3 +- .../ResourceTree/ResourceTreeFactory.cs | 3 +- Penumbra/Interop/Services/CharacterUtility.cs | 3 +- Penumbra/Interop/Services/FontReloader.cs | 3 +- Penumbra/Interop/Services/ModelRenderer.cs | 9 +- Penumbra/Interop/Services/RedrawService.cs | 11 +- .../Services/ResidentResourceManager.cs | 5 +- Penumbra/Meta/MetaFileManager.cs | 3 +- Penumbra/Mods/Editor/DuplicateManager.cs | 3 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 3 +- Penumbra/Mods/Editor/ModEditor.cs | 4 +- Penumbra/Mods/Editor/ModFileCollection.cs | 3 +- Penumbra/Mods/Editor/ModFileEditor.cs | 3 +- Penumbra/Mods/Editor/ModMerger.cs | 3 +- Penumbra/Mods/Editor/ModNormalizer.cs | 4 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 4 +- Penumbra/Mods/Manager/ModCacheManager.cs | 3 +- Penumbra/Mods/Manager/ModDataEditor.cs | 5 +- Penumbra/Mods/Manager/ModExportManager.cs | 3 +- Penumbra/Mods/Manager/ModFileSystem.cs | 5 +- Penumbra/Mods/Manager/ModImportManager.cs | 6 +- Penumbra/Mods/Manager/ModManager.cs | 3 +- Penumbra/Mods/ModCreator.cs | 3 +- Penumbra/Services/StaticServiceManager.cs | 154 +----------------- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 3 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 3 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 5 +- Penumbra/UI/ChangedItemDrawer.cs | 5 +- Penumbra/UI/Changelog.cs | 3 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 3 +- Penumbra/UI/ConfigWindow.cs | 3 +- Penumbra/UI/FileDialogService.cs | 3 +- Penumbra/UI/ImportPopup.cs | 3 +- Penumbra/UI/LaunchButton.cs | 3 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 9 +- Penumbra/UI/ModsTab/ModPanel.cs | 3 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 28 +--- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 18 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 3 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 3 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 4 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 11 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 7 +- Penumbra/UI/PredefinedTagManager.cs | 4 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 3 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 3 +- Penumbra/UI/Tabs/CollectionsTab.cs | 3 +- Penumbra/UI/Tabs/ConfigTabBar.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 6 +- Penumbra/UI/Tabs/EffectiveTab.cs | 3 +- Penumbra/UI/Tabs/MessagesTab.cs | 3 +- Penumbra/UI/Tabs/ModsTab.cs | 3 +- Penumbra/UI/Tabs/OnScreenTab.cs | 10 +- Penumbra/UI/Tabs/ResourceTab.cs | 3 +- Penumbra/UI/Tabs/SettingsTab.cs | 3 +- Penumbra/UI/TutorialService.cs | 3 +- Penumbra/UI/WindowSystem.cs | 3 +- 81 files changed, 220 insertions(+), 317 deletions(-) diff --git a/OtterGui b/OtterGui index e95c0f04..caa9e9b9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit e95c0f04edc7e85aea67498fd8bf495a7fe6d3c8 +Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65 diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index cbb07436..0b52e64a 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Meta.Manipulations; @@ -18,7 +19,7 @@ public enum RedirectResult FilteredGamePath = 3, } -public class TempModManager : IDisposable +public class TempModManager : IDisposable, IService { private readonly CommunicatorService _communicator; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 02c9c8a9..44c12856 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -17,7 +18,7 @@ using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; -public class CollectionCacheManager : IDisposable +public class CollectionCacheManager : IDisposable, IService { private readonly FrameworkManager _framework; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 4e8ebe36..6d48f74b 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; @@ -11,7 +12,7 @@ using Penumbra.UI; namespace Penumbra.Collections.Manager; -public class ActiveCollectionData +public class ActiveCollectionData : IService { public ModCollection Current { get; internal set; } = ModCollection.Empty; public ModCollection Default { get; internal set; } = ModCollection.Empty; @@ -20,7 +21,7 @@ public class ActiveCollectionData public readonly ModCollection?[] SpecialCollections = new ModCollection?[Enum.GetValues().Length - 3]; } -public class ActiveCollections : ISavable, IDisposable +public class ActiveCollections : ISavable, IDisposable, IService { public const int Version = 2; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 0243de1e..caff2c86 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -7,7 +8,7 @@ using Penumbra.Services; namespace Penumbra.Collections.Manager; -public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) +public class CollectionEditor(SaveService saveService, CommunicatorService communicator, ModStorage modStorage) : IService { /// Enable or disable the mod inheritance of mod idx. public bool SetModInheritance(ModCollection collection, Mod mod, bool inherit) diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index e95617b1..85f5b957 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Collections.Cache; namespace Penumbra.Collections.Manager; @@ -8,7 +9,7 @@ public class CollectionManager( InheritanceManager inheritances, CollectionCacheManager caches, TempCollectionManager temp, - CollectionEditor editor) + CollectionEditor editor) : IService { public readonly CollectionStorage Storage = storage; public readonly ActiveCollections Active = active; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 67de3a03..cd680d36 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,7 +1,7 @@ -using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; @@ -11,7 +11,6 @@ using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using Penumbra.UI.CollectionTab; namespace Penumbra.Collections.Manager; @@ -24,7 +23,7 @@ public readonly record struct LocalCollectionId(int Id) : IAdditionOperators new(left.Id + right); } -public class CollectionStorage : IReadOnlyList, IDisposable +public class CollectionStorage : IReadOnlyList, IDisposable, IService { private readonly CommunicatorService _communicator; private readonly SaveService _saveService; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 6003b5f9..f3482cdf 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -2,11 +2,10 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI.CollectionTab; -using Penumbra.Util; namespace Penumbra.Collections.Manager; @@ -15,7 +14,7 @@ namespace Penumbra.Collections.Manager; /// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. /// Circular dependencies are resolved by distinctness. /// -public class InheritanceManager : IDisposable +public class InheritanceManager : IDisposable, IService { public enum ValidInheritance { @@ -144,7 +143,8 @@ public class InheritanceManager : IDisposable continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) { @@ -153,12 +153,14 @@ public class InheritanceManager : IDisposable if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + NotificationType.Warning); } else { Penumbra.Messager.NotificationMessage( - $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", NotificationType.Warning); + $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", + NotificationType.Warning); changes = true; } } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index ce438a6b..5c893232 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Communication; using Penumbra.GameData.Actors; @@ -8,7 +9,7 @@ using Penumbra.String; namespace Penumbra.Collections.Manager; -public class TempCollectionManager : IDisposable +public class TempCollectionManager : IDisposable, IService { public int GlobalChangeCounter { get; private set; } public readonly IndividualCollections Collections; diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 4e1d6453..484dd954 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -3,6 +3,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -10,12 +11,11 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; -using Penumbra.Services; using Penumbra.UI; namespace Penumbra; -public class CommandHandler : IDisposable +public class CommandHandler : IDisposable, IApiService { private const string CommandName = "/penumbra"; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 02286cc7..f6100b62 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Import.Structs; using Penumbra.Interop.Services; @@ -18,7 +19,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; [Serializable] -public class Configuration : IPluginConfiguration, ISavable +public class Configuration : IPluginConfiguration, ISavable, IService { [JsonIgnore] private readonly SaveService _saveService; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 0a542d04..52e276c7 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Enums; @@ -14,7 +15,7 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra; -public class EphemeralConfig : ISavable, IDisposable +public class EphemeralConfig : ISavable, IDisposable, IService { [JsonIgnore] private readonly SaveService _saveService; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9fa77784..01396cfb 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using Lumina.Data.Parsing; using OtterGui; +using OtterGui.Services; using OtterGui.Tasks; using Penumbra.Collections.Manager; using Penumbra.GameData; @@ -21,7 +22,8 @@ namespace Penumbra.Import.Models; using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) + : SingleTaskQueue, IDisposable, IService { private readonly IFramework _framework = framework; diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 976bc179..4aa64209 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Services; using Lumina.Data.Files; using OtterGui.Log; +using OtterGui.Services; using OtterGui.Tasks; using OtterTex; using SixLabors.ImageSharp; @@ -12,22 +13,14 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager : SingleTaskQueue, IDisposable +public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) + : SingleTaskQueue, IDisposable, IService { - private readonly Logger _logger; - private readonly UiBuilder _uiBuilder; - private readonly IDataManager _gameData; + private readonly Logger _logger = logger; - private readonly ConcurrentDictionary _tasks = new(); + private readonly ConcurrentDictionary _tasks = new(); private bool _disposed; - public TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) - { - _uiBuilder = uiBuilder; - _gameData = gameData; - _logger = logger; - } - public IReadOnlyDictionary Tasks => _tasks; @@ -64,7 +57,8 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable { var token = new CancellationTokenSource(); var task = Enqueue(a, token.Token); - task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + task.ContinueWith(_ => _tasks.TryRemove(a, out var unused), CancellationToken.None, TaskContinuationOptions.None, + TaskScheduler.Default); return (task, token); }).Item1; } @@ -217,7 +211,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable /// Load a texture wrap for a given image. public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height) - => _uiBuilder.LoadImageRaw(rgba, width, height, 4); + => uiBuilder.LoadImageRaw(rgba, width, height, 4); /// Load any supported file from game data or drive depending on extension and if the path is rooted. public (BaseImage, TextureType) Load(string path) @@ -326,7 +320,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable } public bool GameFileExists(string path) - => _gameData.FileExists(path); + => gameData.FileExists(path); /// Add up to 13 mip maps to the input if mip maps is true, otherwise return input. public static ScratchImage AddMipMaps(ScratchImage input, bool mipMaps) @@ -382,7 +376,7 @@ public sealed class TextureManager : SingleTaskQueue, IDisposable if (Path.IsPathRooted(path)) return File.OpenRead(path); - var file = _gameData.GetFile(path); + var file = gameData.GetFile(path); return file != null ? new MemoryStream(file.Data) : throw new Exception($"Unable to obtain \"{path}\" from game files."); } diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 93fee11e..feb27341 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; @@ -9,7 +8,7 @@ using Penumbra.String; namespace Penumbra.Interop.PathResolving; -public sealed class CutsceneService : IService, IDisposable +public sealed class CutsceneService : IRequiredService, IDisposable { public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart; public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 32090f7c..eeff7eee 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -10,7 +11,8 @@ using Penumbra.Services; namespace Penumbra.Interop.PathResolving; -public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> +public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>, + IService { private readonly CommunicatorService _communicator; private readonly CharacterDestructor _characterDestructor; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 7f820b4e..f7dcfc07 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData.Structs; @@ -34,7 +35,7 @@ namespace Penumbra.Interop.PathResolving; // ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight. // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor. -public sealed unsafe class MetaState : IDisposable +public sealed unsafe class MetaState : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index f31b3323..cc3e0e9b 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,4 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -9,7 +10,7 @@ using Penumbra.Util; namespace Penumbra.Interop.PathResolving; -public class PathResolver : IDisposable +public class PathResolver : IDisposable, IService { private readonly PerformanceTracker _performance; private readonly Configuration _config; diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index f4218e9c..bf9d1e25 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Interop.Services; using Penumbra.String; @@ -5,7 +6,7 @@ using Penumbra.String; namespace Penumbra.Interop.PathResolving; public sealed class PathState(CollectionResolver collectionResolver, MetaState metaState, CharacterUtility characterUtility) - : IDisposable + : IDisposable, IService { public readonly CollectionResolver CollectionResolver = collectionResolver; public readonly MetaState MetaState = metaState; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 3cefd98d..44a152f0 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,9 +1,9 @@ +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; -using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.PathResolving; @@ -13,7 +13,7 @@ namespace Penumbra.Interop.PathResolving; /// Those are loaded synchronously. /// Thus, we need to ensure the correct files are loaded when a material is loaded. /// -public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection> +public sealed unsafe class SubfileHelper : IDisposable, IReadOnlyCollection>, IService { private readonly GameState _gameState; private readonly ResourceLoader _loader; diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index 64442771..f1d7fe24 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -1,13 +1,14 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.Util; namespace Penumbra.Interop.ResourceLoading; -public unsafe class FileReadService : IDisposable +public unsafe class FileReadService : IDisposable, IRequiredService { public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs index 7b49beab..4a423993 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceLoader.cs @@ -1,4 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.PathResolving; @@ -10,7 +11,7 @@ using FileMode = Penumbra.Interop.Structs.FileMode; namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceLoader : IDisposable +public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; @@ -212,7 +213,7 @@ public unsafe class ResourceLoader : IDisposable /// /// Catch weird errors with invalid decrements of the reference count. /// - private void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) + private static void DecRefProtection(ResourceHandle* handle, ref byte? returnValue) { if (handle->RefCount != 0) return; diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index a087a659..c885c317 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -4,12 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using FFXIVClientStructs.STD; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceManagerService +public unsafe class ResourceManagerService : IRequiredService { public ResourceManagerService(IGameInteropProvider interop) => interop.InitializeFromAttributes(this); diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index e3338e6c..54c86777 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.Interop.SafeHandles; @@ -13,7 +14,7 @@ using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle. namespace Penumbra.Interop.ResourceLoading; -public unsafe class ResourceService : IDisposable +public unsafe class ResourceService : IDisposable, IRequiredService { private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index b9279f54..e617673e 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -2,13 +2,14 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceLoading; -public unsafe class TexMdlService : IDisposable +public unsafe class TexMdlService : IDisposable, IRequiredService { /// Custom ulong flag to signal our files as opposed to SE files. public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 5a190e52..e26c1436 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; using Penumbra.GameData.Data; @@ -17,7 +18,7 @@ public class ResourceTreeFactory( ObjectIdentification identifier, Configuration config, ActorManager actors, - PathState pathState) + PathState pathState) : IService { private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 0877d221..532dc823 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,11 +1,12 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.Structs; namespace Penumbra.Interop.Services; -public unsafe class CharacterUtility : IDisposable +public unsafe class CharacterUtility : IDisposable, IRequiredService { public record struct InternalIndex(int Value); diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 2f4a3cfd..259fdd10 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Component.GUI; +using OtterGui.Services; using Penumbra.GameData; namespace Penumbra.Interop.Services; @@ -9,7 +10,7 @@ namespace Penumbra.Interop.Services; /// Handle font reloading via game functions. /// May cause a interface flicker while reloading. /// -public unsafe class FontReloader +public unsafe class FontReloader : IService { public bool Valid => _reloadFontsFunc != null; diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 7df83cf7..b268b395 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -1,10 +1,11 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Services; namespace Penumbra.Interop.Services; -public unsafe class ModelRenderer : IDisposable +public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } @@ -37,14 +38,14 @@ public unsafe class ModelRenderer : IDisposable if (DefaultCharacterGlassShaderPackage == null) { - DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; - anyMissing |= DefaultCharacterGlassShaderPackage == null; + DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; + anyMissing |= DefaultCharacterGlassShaderPackage == null; } if (anyMissing) return; - Ready = true; + Ready = true; _framework.Update -= LoadDefaultResources; } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 21ecfd4f..61d7b90c 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Housing; using FFXIVClientStructs.Interop; +using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Communication; @@ -20,7 +21,7 @@ using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Services; -public unsafe partial class RedrawService +public unsafe partial class RedrawService : IService { public const int GPosePlayerIdx = 201; public const int GPoseSlots = 42; @@ -171,7 +172,8 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) DisableDraw(actor!); - if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) + if (actor is PlayerCharacter + && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) |= DrawState.Invisibility; if (gPose) @@ -190,7 +192,8 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) EnableDraw(actor!); - if (actor is PlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) + if (actor is PlayerCharacter + && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility; if (gPose) @@ -380,7 +383,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (!ret && lowerName.Length > 1 && lowerName[0] == '#' && ushort.TryParse(lowerName[1..], out var objectIndex)) { ret = true; - actor = _objects.GetDalamudObject((int) objectIndex); + actor = _objects.GetDalamudObject((int)objectIndex); } return ret; diff --git a/Penumbra/Interop/Services/ResidentResourceManager.cs b/Penumbra/Interop/Services/ResidentResourceManager.cs index 72697185..4f430aa1 100644 --- a/Penumbra/Interop/Services/ResidentResourceManager.cs +++ b/Penumbra/Interop/Services/ResidentResourceManager.cs @@ -1,10 +1,11 @@ -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OtterGui.Services; using Penumbra.GameData; namespace Penumbra.Interop.Services; -public unsafe class ResidentResourceManager +public unsafe class ResidentResourceManager : IService { // A static pointer to the resident resource manager address. [Signature(Sigs.ResidentResourceManager, ScanType = ScanType.StaticAddress)] diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 81c0fa3e..3755afa2 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Data; @@ -13,7 +14,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage namespace Penumbra.Meta; -public unsafe class MetaFileManager +public class MetaFileManager : IService { internal readonly Configuration Config; internal readonly CharacterUtility CharacterUtility; diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 47aa18dc..bcecf264 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -1,4 +1,5 @@ using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -7,7 +8,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) +public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config) : IService { private readonly SHA256 _hasher = SHA256.Create(); private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = []; diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 738e606e..2a23ffad 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,11 +1,12 @@ using OtterGui; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; namespace Penumbra.Mods.Editor; -public partial class MdlMaterialEditor(ModFileCollection files) +public partial class MdlMaterialEditor(ModFileCollection files) : IService { [GeneratedRegex(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.ExplicitCapture | RegexOptions.NonBacktracking)] private static partial Regex MaterialRegex(); diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 37524da1..cacb7f88 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.SubMods; @@ -14,7 +14,7 @@ public class ModEditor( ModSwapEditor swapEditor, MdlMaterialEditor mdlMaterialEditor, FileCompactor compactor) - : IDisposable + : IDisposable, IService { public readonly ModNormalizer ModNormalizer = modNormalizer; public readonly ModMetaEditor MetaEditor = metaEditor; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 551d04cf..241f5b3b 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,10 +1,11 @@ using OtterGui; +using OtterGui.Services; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileCollection : IDisposable +public class ModFileCollection : IDisposable, IService { private readonly List _available = []; private readonly List _mtrl = []; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index e2c0b726..55e0e94e 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.Services; @@ -5,7 +6,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) +public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) : IService { public bool Changes { get; private set; } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 2df76838..9d31664b 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -13,7 +14,7 @@ using Penumbra.UI.ModsTab; namespace Penumbra.Mods.Editor; -public class ModMerger : IDisposable +public class ModMerger : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 58e4fc08..c6bc4939 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,15 +1,15 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; +using OtterGui.Services; using OtterGui.Tasks; -using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModNormalizer(ModManager _modManager, Configuration _config) +public class ModNormalizer(ModManager _modManager, Configuration _config) : IService { private readonly List>> _redirections = []; diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0250efae..1a8ff2eb 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -1,10 +1,10 @@ -using Penumbra.Mods; +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; using Penumbra.Util; -public class ModSwapEditor(ModManager modManager) +public class ModSwapEditor(ModManager modManager) : IService { private readonly Dictionary _swaps = []; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 8ab8cf33..38d98d7c 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.Mods.Groups; @@ -8,7 +9,7 @@ using Penumbra.Util; namespace Penumbra.Mods.Manager; -public class ModCacheManager : IDisposable +public class ModCacheManager : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index c7c7c2cc..4ab9deb1 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,6 +1,7 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -23,7 +24,7 @@ public enum ModDataChangeType : ushort Note = 0x0800, } -public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService { /// 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, @@ -49,7 +50,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var save = true; if (File.Exists(dataFile)) - { try { var text = File.ReadAllText(dataFile); @@ -65,7 +65,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic { Penumbra.Log.Error($"Could not load local mod data:\n{e}"); } - } if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); diff --git a/Penumbra/Mods/Manager/ModExportManager.cs b/Penumbra/Mods/Manager/ModExportManager.cs index 676018be..38b9c0fd 100644 --- a/Penumbra/Mods/Manager/ModExportManager.cs +++ b/Penumbra/Mods/Manager/ModExportManager.cs @@ -1,10 +1,11 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ModExportManager : IDisposable +public class ModExportManager : IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index c8a0a5db..e32fec0c 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,12 +1,11 @@ -using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.Mods.Manager; -public sealed class ModFileSystem : FileSystem, IDisposable, ISavable +public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, IService { private readonly ModManager _modManager; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index c99b7d0e..ff39b021 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,11 +1,12 @@ using Dalamud.Interface.Internal.Notifications; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Import; using Penumbra.Mods.Editor; namespace Penumbra.Mods.Manager; -public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable, IService { private readonly ConcurrentQueue _modsToUnpack = new(); @@ -32,7 +33,8 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd if (File.Exists(s)) return true; - Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, false); + Penumbra.Messager.NotificationMessage($"Failed to import queued mod at {s}, the file does not exist.", NotificationType.Warning, + false); return false; }).Select(s => new FileInfo(s)).ToArray(); diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 42082383..4b19ea4c 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager.OptionEditor; @@ -27,7 +28,7 @@ public enum ModPathChangeType StartingReload, } -public sealed class ModManager : ModStorage, IDisposable +public sealed class ModManager : ModStorage, IDisposable, IService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index ed4245c4..0e66367a 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Import; @@ -23,7 +24,7 @@ public partial class ModCreator( Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, - GamePathParser _gamePathParser) + GamePathParser _gamePathParser) : IService { public readonly Configuration Config = config; diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 35e36349..3279da96 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -5,36 +5,15 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using OtterGui; -using OtterGui.Classes; using OtterGui.Log; using OtterGui.Services; -using Penumbra.Api; using Penumbra.Api.Api; -using Penumbra.Collections.Cache; -using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.Import.Models; using Penumbra.GameData.Structs; -using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; -using Penumbra.Interop.ResourceTree; -using Penumbra.Interop.Services; using Penumbra.Meta; -using Penumbra.Mods; -using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; -using Penumbra.UI; -using Penumbra.UI.AdvancedWindow; -using Penumbra.UI.Classes; -using Penumbra.UI.ModsTab; -using Penumbra.UI.ResourceWatcher; -using Penumbra.UI.Tabs; -using Penumbra.UI.Tabs.Debug; using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; -using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; -using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; -using Penumbra.Api.IpcTester; namespace Penumbra.Services; @@ -45,19 +24,18 @@ public static class StaticServiceManager var services = new ServiceManager(log) .AddDalamudServices(pi) .AddExistingService(log) - .AddExistingService(penumbra) - .AddInterop() - .AddConfiguration() - .AddCollections() - .AddMods() - .AddResources() - .AddResolvers() - .AddInterface() - .AddModEditor() - .AddApi(); + .AddExistingService(penumbra); services.AddIServices(typeof(EquipItem).Assembly); services.AddIServices(typeof(Penumbra).Assembly); services.AddIServices(typeof(ImGuiUtil).Assembly); + services.AddSingleton(p => + { + var cutsceneService = p.GetRequiredService(); + return new CutsceneResolver(cutsceneService.GetParentIndex); + }) + .AddSingleton(p => p.GetRequiredService().ImcChecker) + .AddSingleton(s => (ModStorage)s.GetRequiredService()) + .AddSingleton(x => x.GetRequiredService()); services.CreateProvider(); return services; } @@ -83,118 +61,4 @@ public static class StaticServiceManager .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi); - - private static ServiceManager AddInterop(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(p => - { - var cutsceneService = p.GetRequiredService(); - return new CutsceneResolver(cutsceneService.GetParentIndex); - }) - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(p => p.GetRequiredService().ImcChecker); - - private static ServiceManager AddConfiguration(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddCollections(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddMods(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(s => (ModStorage)s.GetRequiredService()); - - private static ServiceManager AddResources(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddResolvers(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddInterface(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(p => new Diagnostics(p)); - - private static ServiceManager AddModEditor(this ServiceManager services) - => services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - private static ServiceManager AddApi(this ServiceManager services) - => services.AddSingleton(x => x.GetRequiredService()); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 62115dd6..652f928d 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -24,7 +25,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class ItemSwapTab : IDisposable, ITab +public class ItemSwapTab : IDisposable, ITab, IUiService { private readonly Configuration _config; private readonly CommunicatorService _communicator; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 9410b793..90fdc48e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -32,7 +33,7 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; -public partial class ModEditWindow : Window, IDisposable +public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 5dad66b4..b5f0255c 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -9,7 +10,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class ModMergeTab(ModMerger modMerger) +public class ModMergeTab(ModMerger modMerger) : IUiService { private readonly ModCombo _modCombo = new(() => modMerger.ModsWithoutCurrent.ToList()); private string _newModName = string.Empty; @@ -183,7 +184,7 @@ public class ModMergeTab(ModMerger modMerger) else { ImGuiUtil.DrawTableColumn(option.GetName()); - + ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 29a1f291..0afeeeeb 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -10,6 +10,7 @@ using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -19,7 +20,7 @@ using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; -public class ChangedItemDrawer : IDisposable +public class ChangedItemDrawer : IDisposable, IUiService { [Flags] public enum ChangedItemIcon : uint @@ -99,8 +100,10 @@ public class ChangedItemDrawer : IDisposable slot = 0; foreach (var (item, flag) in LowerNames.Zip(Order)) + { if (item.Contains(lowerInput, StringComparison.Ordinal)) slot |= flag; + } return slot != 0; } diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 184633f2..f4cedf7d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -1,8 +1,9 @@ +using OtterGui.Services; using OtterGui.Widgets; namespace Penumbra.UI; -public class PenumbraChangelog +public class PenumbraChangelog : IUiService { public const int LastChangelogVersion = 0; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index bff0092a..6c8bbf64 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,6 +1,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -9,7 +10,7 @@ using Penumbra.UI.ModsTab; namespace Penumbra.UI.Classes; -public class CollectionSelectHeader +public class CollectionSelectHeader : IUiService { private readonly CollectionCombo _collectionCombo; private readonly ActiveCollections _activeCollections; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 0ae16f6d..67b0a50c 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -4,6 +4,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Services; @@ -13,7 +14,7 @@ using Penumbra.Util; namespace Penumbra.UI; -public sealed class ConfigWindow : Window +public sealed class ConfigWindow : Window, IUiService { private readonly DalamudPluginInterface _pluginInterface; private readonly Configuration _config; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 88c0b00f..cc2a7f6a 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -3,12 +3,13 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; namespace Penumbra.UI; -public class FileDialogService : IDisposable +public class FileDialogService : IDisposable, IUiService { private readonly CommunicatorService _communicator; private readonly FileDialogManager _manager; diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index 71164d1d..fb2028b5 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,13 +1,14 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Import.Structs; using Penumbra.Mods.Manager; namespace Penumbra.UI; /// Draw the progress information for import. -public sealed class ImportPopup : Window +public sealed class ImportPopup : Window, IUiService { public const string WindowLabel = "Penumbra Import Status"; diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 9650ccf8..14e16432 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using OtterGui.Services; namespace Penumbra.UI; @@ -9,7 +10,7 @@ namespace Penumbra.UI; /// A Launch Button used in the title screen of the game, /// using the Dalamud-provided collapsible submenu. /// -public class LaunchButton : IDisposable +public class LaunchButton : IDisposable, IUiService { private readonly ConfigWindow _configWindow; private readonly UiBuilder _uiBuilder; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 58f0b615..0ca4d40c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -21,7 +22,7 @@ using MessageService = Penumbra.Services.MessageService; namespace Penumbra.UI.ModsTab; -public sealed class ModFileSystemSelector : FileSystemSelector +public sealed class ModFileSystemSelector : FileSystemSelector, IUiService { private readonly CommunicatorService _communicator; private readonly MessageService _messager; @@ -33,9 +34,9 @@ public sealed class ModFileSystemSelector : FileSystemSelector().Aggregate((a, b) => a | b); - public ReadOnlySpan Label => "Changed Items"u8; - public ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) - { - _selector = selector; - _drawer = drawer; - } - public bool IsVisible - => _selector.Selected!.ChangedItems.Count > 0; + => selector.Selected!.ChangedItems.Count > 0; public void DrawContent() { - _drawer.DrawTypeFilter(); + drawer.DrawTypeFilter(); ImGui.Separator(); using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); if (!table) return; - var zipList = ZipList.FromSortedList((SortedList)_selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList((SortedList)selector.Selected!.ChangedItems); var height = ImGui.GetFrameHeightWithSpacing(); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(height); @@ -43,14 +33,14 @@ public class ModPanelChangedItemsTab : ITab } private bool CheckFilter((string Name, object? Data) kvp) - => _drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); private void DrawChangedItem((string Name, object? Data) kvp) { ImGui.TableNextColumn(); - _drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + drawer.DrawCategoryIcon(kvp.Name, kvp.Data); ImGui.SameLine(); - _drawer.DrawChangedItem(kvp.Name, kvp.Data); - _drawer.DrawModelData(kvp.Data); + drawer.DrawChangedItem(kvp.Name, kvp.Data); + drawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index aa598557..9f37f847 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -10,25 +11,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public class ModPanelCollectionsTab : ITab +public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) : ITab, IUiService { - private readonly ModFileSystemSelector _selector; - private readonly CollectionStorage _collections; - - private readonly List<(ModCollection, ModCollection, uint, string)> _cache = new(); - - public ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) - { - _collections = storage; - _selector = selector; - } + private readonly List<(ModCollection, ModCollection, uint, string)> _cache = []; public ReadOnlySpan Label => "Collections"u8; public void DrawContent() { - var (direct, inherited) = CountUsage(_selector.Selected!); + var (direct, inherited) = CountUsage(selector.Selected!); ImGui.NewLine(); if (direct == 1) ImGui.TextUnformatted("This Mod is directly configured in 1 collection."); @@ -80,7 +72,7 @@ public class ModPanelCollectionsTab : ITab var disInherited = ColorId.InheritedDisabledMod.Value(); var directCount = 0; var inheritedCount = 0; - foreach (var collection in _collections) + foreach (var collection in storage) { var (settings, parent) = collection[mod.Index]; var (color, text) = settings == null diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index c1a3c1eb..bee48068 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; @@ -16,7 +17,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab +public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector) : ITab, IUiService { private int? _currentPriority; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index ed6340ab..6fe3e4c6 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Mods.Manager; @@ -12,7 +13,7 @@ public class ModPanelDescriptionTab( TutorialService tutorial, ModManager modManager, PredefinedTagManager predefinedTagsConfig) - : ITab + : ITab, IUiService { private readonly TagButtons _localTags = new(); private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 468e97b9..1e371065 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -6,13 +6,13 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; -using Penumbra.Mods.Manager.OptionEditor; using Penumbra.UI.ModsTab.Groups; namespace Penumbra.UI.ModsTab; @@ -31,7 +31,7 @@ public class ModPanelEditTab( ModGroupEditDrawer groupEditDrawer, DescriptionEditPopup descriptionPopup, AddGroupDrawer addGroupDrawer) - : ITab + : ITab, IUiService { private readonly TagButtons _modTags = new(); diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 8b09d8b9..639118f5 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -9,7 +10,7 @@ using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; -public class ModPanelTabBar +public class ModPanelTabBar : IUiService { private enum ModPanelTabType { @@ -33,7 +34,7 @@ public class ModPanelTabBar public readonly ITab[] Tabs; private ModPanelTabType _preferredTab = ModPanelTabType.Settings; - private Mod? _lastMod = null; + private Mod? _lastMod; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, @@ -49,15 +50,15 @@ public class ModPanelTabBar _tutorial = tutorial; Collections = collections; - Tabs = new ITab[] - { + Tabs = + [ Settings, Description, Conflicts, ChangedItems, Collections, Edit, - }; + ]; } public void Draw(Mod mod) diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 595240f4..4079748e 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -3,12 +3,13 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) +public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) : IUiService { public void Draw() { @@ -65,8 +66,8 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito } private string _tag = string.Empty; - private readonly List _addMods = []; - private readonly List<(Mod, int)> _removeMods = []; + private readonly List _addMods = []; + private readonly List<(Mod, int)> _removeMods = []; private void DrawMultiTagger() { diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 0e5377d6..d531b1a2 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -6,14 +6,14 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; -using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class PredefinedTagManager : ISavable, IReadOnlyList +public sealed class PredefinedTagManager : ISavable, IReadOnlyList, IService { public const int Version = 1; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 65a8fe76..a7d1a8c6 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -15,7 +16,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ResourceWatcher; -public sealed class ResourceWatcher : IDisposable, ITab +public sealed class ResourceWatcher : IDisposable, ITab, IUiService { public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index ab0badf4..2aeaaea0 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -17,7 +18,7 @@ public class ChangedItemsTab( CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer, CommunicatorService communicator) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Changed Items"u8; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index fabf7561..34e2cbcf 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; @@ -11,7 +12,7 @@ using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; -public sealed class CollectionsTab : IDisposable, ITab +public sealed class CollectionsTab : IDisposable, ITab, IUiService { private readonly EphemeralConfig _config; private readonly CollectionSelector _selector; diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs index 9fd07f27..28827ad9 100644 --- a/Penumbra/UI/Tabs/ConfigTabBar.cs +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; @@ -8,7 +9,7 @@ using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher; namespace Penumbra.UI.Tabs; -public class ConfigTabBar : IDisposable +public class ConfigTabBar : IDisposable, IUiService { private readonly CommunicatorService _communicator; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 1519ebf0..0122a6f5 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -44,7 +44,7 @@ using Penumbra.Api.IpcTester; namespace Penumbra.UI.Tabs.Debug; -public class Diagnostics(IServiceProvider provider) +public class Diagnostics(ServiceManager provider) : IUiService { public void DrawDiagnostics() { @@ -55,7 +55,7 @@ public class Diagnostics(IServiceProvider provider) foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { - var container = (IAsyncDataContainer)provider.GetRequiredService(type); + var container = (IAsyncDataContainer)provider.Provider!.GetRequiredService(type); ImGuiUtil.DrawTableColumn(container.Name); ImGuiUtil.DrawTableColumn(container.Time.ToString()); ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory)); @@ -64,7 +64,7 @@ public class Diagnostics(IServiceProvider provider) } } -public class DebugTab : Window, ITab +public class DebugTab : Window, ITab, IUiService { private readonly PerformanceTracker _performance; private readonly Configuration _config; diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 7076b80f..1b9af75c 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -15,7 +16,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs; public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Effective Changes"u8; diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/Tabs/MessagesTab.cs index abaf2ba6..190f9407 100644 --- a/Penumbra/UI/Tabs/MessagesTab.cs +++ b/Penumbra/UI/Tabs/MessagesTab.cs @@ -1,9 +1,10 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Services; namespace Penumbra.UI.Tabs; -public class MessagesTab(MessageService messages) : ITab +public class MessagesTab(MessageService messages) : ITab, IUiService { public ReadOnlySpan Label => "Messages"u8; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index e4d94bb5..7faa3da8 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -6,6 +6,7 @@ using Penumbra.UI.Classes; using Dalamud.Interface; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Housing; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Interop.Services; @@ -30,7 +31,7 @@ public class ModsTab( CollectionSelectHeader collectionHeader, ITargetManager targets, ObjectManager objects) - : ITab + : ITab, IUiService { private readonly ActiveCollections _activeCollections = collectionManager.Active; diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/Tabs/OnScreenTab.cs index 787e07a1..fa33f702 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/Tabs/OnScreenTab.cs @@ -1,16 +1,12 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.Tabs; -public class OnScreenTab : ITab +public class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab, IUiService { - private readonly ResourceTreeViewer _viewer; - - public OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) - { - _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); - } + private readonly ResourceTreeViewer _viewer = resourceTreeViewerFactory.Create(0, delegate { }, delegate { }); public ReadOnlySpan Label => "On-Screen"u8; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index bbb0561b..0b54c5e2 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -6,6 +6,7 @@ using FFXIVClientStructs.STD; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; @@ -13,7 +14,7 @@ using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) - : ITab + : ITab, IUiService { public ReadOnlySpan Label => "Resource Manager"u8; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 0de4f790..17db21c9 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -9,6 +9,7 @@ using OtterGui; using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Interop.Services; @@ -19,7 +20,7 @@ using Penumbra.UI.ModsTab; namespace Penumbra.UI.Tabs; -public class SettingsTab : ITab +public class SettingsTab : ITab, IUiService { public const int RootDirectoryMaxLength = 64; diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index d87df19e..7d2a0d2a 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -1,3 +1,4 @@ +using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -40,7 +41,7 @@ public enum BasicTutorialSteps } /// Service for the in-game tutorial. -public class TutorialService +public class TutorialService : IUiService { public const string SelectedCollection = "Selected Collection"; public const string DefaultCollection = "Base Collection"; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index c5418eb3..99819fce 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -1,12 +1,13 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; +using OtterGui.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Tabs.Debug; namespace Penumbra.UI; -public class PenumbraWindowSystem : IDisposable +public class PenumbraWindowSystem : IDisposable, IUiService { private readonly UiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; From 90124e83df4dfc6c21c5a8107e0d749229df2309 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 19 Jun 2024 11:51:55 +0000 Subject: [PATCH 189/865] [CI] Updating repo.json for testing_1.1.1.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 318aafc2..fc7234bd 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.1", - "TestingAssemblyVersion": "1.1.1.1", + "TestingAssemblyVersion": "1.1.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a90e253c73b6bd6e68f37f911e7be3f2c5e0a69b Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:54:17 +0200 Subject: [PATCH 190/865] Update repo.json --- repo.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/repo.json b/repo.json index fc7234bd..c00e3c8f 100644 --- a/repo.json +++ b/repo.json @@ -5,7 +5,7 @@ "Punchline": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", - "AssemblyVersion": "1.1.1.1", + "AssemblyVersion": "1.1.1.2", "TestingAssemblyVersion": "1.1.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", @@ -17,9 +17,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 29f8c91306daafc4bc5eab156015c895c8aa9a21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 19 Jun 2024 22:34:59 +0200 Subject: [PATCH 191/865] Make meta hooks respect Enable Mod setting and fix EQP composition. --- OtterGui | 2 +- Penumbra/Collections/Cache/EqpCache.cs | 49 +++++++++++++++++-- Penumbra/Configuration.cs | 16 +++++- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/EstHook.cs | 13 +++-- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 11 +++-- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 8 ++- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 15 ++++-- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 8 ++- Penumbra/Interop/PathResolving/MetaState.cs | 8 +-- 12 files changed, 121 insertions(+), 33 deletions(-) diff --git a/OtterGui b/OtterGui index caa9e9b9..6fafc03b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit caa9e9b9a5dc3928eba10b315cf6a0f6f1d84b65 +Subproject commit 6fafc03b34971be7c0e74fd9a638d1ed642ea19a diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index 60e38aef..c681b230 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -9,11 +9,24 @@ namespace Penumbra.Collections.Cache; public sealed class EqpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public unsafe EqpEntry GetValues(CharacterArmor* armor) - => GetSingleValue(armor[0].Set, EquipSlot.Head) - | GetSingleValue(armor[1].Set, EquipSlot.Body) - | GetSingleValue(armor[2].Set, EquipSlot.Hands) - | GetSingleValue(armor[3].Set, EquipSlot.Legs) - | GetSingleValue(armor[4].Set, EquipSlot.Feet); + { + var bodyEntry = GetSingleValue(armor[1].Set, EquipSlot.Body); + var headEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHead) + ? GetSingleValue(armor[0].Set, EquipSlot.Head) + : GetSingleValue(armor[1].Set, EquipSlot.Head); + var handEntry = bodyEntry.HasFlag(EqpEntry.BodyShowHand) + ? GetSingleValue(armor[2].Set, EquipSlot.Hands) + : GetSingleValue(armor[1].Set, EquipSlot.Hands); + var (legsEntry, legsId) = bodyEntry.HasFlag(EqpEntry.BodyShowLeg) + ? (GetSingleValue(armor[3].Set, EquipSlot.Legs), 3) + : (GetSingleValue(armor[1].Set, EquipSlot.Legs), 1); + var footEntry = legsEntry.HasFlag(EqpEntry.LegsShowFoot) + ? GetSingleValue(armor[4].Set, EquipSlot.Feet) + : GetSingleValue(armor[legsId].Set, EquipSlot.Feet); + + var combined = bodyEntry | headEntry | handEntry | legsEntry | footEntry; + return PostProcessFeet(PostProcessHands(combined)); + } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private EqpEntry GetSingleValue(PrimaryId id, EquipSlot slot) @@ -24,4 +37,30 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) protected override void Dispose(bool _) => Clear(); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static EqpEntry PostProcessHands(EqpEntry entry) + { + if (!entry.HasFlag(EqpEntry.HandsHideForearm)) + return entry; + + var testFlag = entry.HasFlag(EqpEntry.HandsHideElbow) + ? entry.HasFlag(EqpEntry.BodyHideGlovesL) + : entry.HasFlag(EqpEntry.BodyHideGlovesM); + return testFlag + ? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS + : entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static EqpEntry PostProcessFeet(EqpEntry entry) + { + if (!entry.HasFlag(EqpEntry.FeetHideCalf)) + return entry; + + if (entry.HasFlag(EqpEntry.FeetHideKnee) || !entry.HasFlag(EqpEntry._20)) + return entry & ~(EqpEntry.LegsHideBootsS | EqpEntry.LegsHideBootsM); + + return (entry | EqpEntry.LegsHideBootsM) & ~EqpEntry.LegsHideBootsS; + } } diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f6100b62..7faed6a2 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -31,7 +31,21 @@ public class Configuration : IPluginConfiguration, ISavable, IService public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; - public bool EnableMods { get; set; } = true; + public event Action? ModsEnabled; + + [JsonIgnore] + private bool _enableMods = true; + + public bool EnableMods + { + get => _enableMods; + set + { + _enableMods = value; + ModsEnabled?.Invoke(value); + } + } + public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index aaaaccd4..583a2ac5 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -6,7 +6,7 @@ using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class EqdpAccessoryHook : FastHook +public unsafe class EqdpAccessoryHook : FastHook, IDisposable { public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); @@ -15,7 +15,8 @@ public unsafe class EqdpAccessoryHook : FastHook public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, true); + Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) @@ -27,4 +28,7 @@ public unsafe class EqdpAccessoryHook : FastHook Penumbra.Log.Excessive( $"[GetEqdpAccessoryEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 2711f195..f5488f80 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -6,7 +6,7 @@ using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class EqdpEquipHook : FastHook +public unsafe class EqdpEquipHook : FastHook, IDisposable { public delegate void Delegate(CharacterUtility* utility, EqdpEntry* entry, uint id, uint raceCode); @@ -15,7 +15,8 @@ public unsafe class EqdpEquipHook : FastHook public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, true); + Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) @@ -27,4 +28,7 @@ public unsafe class EqdpEquipHook : FastHook Penumbra.Log.Excessive( $"[GetEqdpEquipEntry] Invoked on 0x{(ulong)utility:X} with {setId}, {(GenderRace)raceCode}, returned {(ushort)*entry:B10}."); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 7107e26b..e96d8115 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -5,7 +5,7 @@ using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class EqpHook : FastHook +public unsafe class EqpHook : FastHook, IDisposable { public delegate void Delegate(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor); @@ -14,7 +14,8 @@ public unsafe class EqpHook : FastHook public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, true); + Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) @@ -31,4 +32,7 @@ public unsafe class EqpHook : FastHook Penumbra.Log.Excessive($"[GetEqpFlags] Invoked on 0x{(nint)utility:X} with 0x{(ulong)armor:X}, returned 0x{(ulong)*flags:X16}."); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 23931182..fa0bb3c5 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -6,7 +6,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public class EstHook : FastHook +public class EstHook : FastHook, IDisposable { public delegate EstEntry Delegate(uint id, int estType, uint genderRace); @@ -14,14 +14,16 @@ public class EstHook : FastHook public EstHook(HookManager hooks, MetaState metaState) { - _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, true); + _metaState = metaState; + Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private EstEntry Detour(uint genderRace, int estType, uint id) { EstEntry ret; - if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache } + if (_metaState.EstCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) ret = entry.Entry; else @@ -46,4 +48,7 @@ public class EstHook : FastHook }; return new EstIdentifier(i, type, gr); } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 256d8702..44d35f12 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -5,7 +5,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class GmpHook : FastHook +public unsafe class GmpHook : FastHook, IDisposable { public delegate nint Delegate(nint gmpResource, uint dividedHeadId); @@ -16,7 +16,8 @@ public unsafe class GmpHook : FastHook public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, true); + Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } /// @@ -27,7 +28,8 @@ public unsafe class GmpHook : FastHook private nint Detour(nint gmpResource, uint dividedHeadId) { nint ret; - if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } + if (_metaState.GmpCollection.TryPeek(out var collection) + && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) { if (entry.Entry.Enabled) @@ -61,4 +63,7 @@ public unsafe class GmpHook : FastHook Marshal.FreeHGlobal((nint)Pointer); } } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index 86759460..db22d90c 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -7,7 +7,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public unsafe class RspBustHook : FastHook +public unsafe class RspBustHook : FastHook, IDisposable { public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize); @@ -19,7 +19,8 @@ public unsafe class RspBustHook : FastHook { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, true); + Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) @@ -63,4 +64,7 @@ public unsafe class RspBustHook : FastHook $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index cf88c34a..dcb3f19c 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -1,4 +1,3 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; @@ -8,7 +7,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public class RspHeightHook : FastHook +public class RspHeightHook : FastHook, IDisposable { public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); @@ -17,15 +16,18 @@ public class RspHeightHook : FastHook public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) { - _metaState = metaState; + _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, true); + Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) { float scale; - if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 + && _metaState.RspCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 @@ -66,4 +68,7 @@ public class RspHeightHook : FastHook $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); return scale; } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index e40f0161..8d333575 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -7,7 +7,7 @@ using Penumbra.Meta.Manipulations; namespace Penumbra.Interop.Hooks.Meta; -public class RspTailHook : FastHook +public class RspTailHook : FastHook, IDisposable { public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); @@ -18,7 +18,8 @@ public class RspTailHook : FastHook { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, true); + Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, metaState.Config.EnableMods); + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) @@ -65,4 +66,7 @@ public class RspTailHook : FastHook $"[GetRspTail] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {tailLength}, returned {scale}."); return scale; } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index f7dcfc07..5eacbfb0 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -37,7 +37,7 @@ namespace Penumbra.Interop.PathResolving; // GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which is SetupVisor. public sealed unsafe class MetaState : IDisposable, IService { - private readonly Configuration _config; + public readonly Configuration Config; private readonly CommunicatorService _communicator; private readonly CollectionResolver _collectionResolver; private readonly ResourceLoader _resources; @@ -64,7 +64,7 @@ public sealed unsafe class MetaState : IDisposable, IService _resources = resources; _createCharacterBase = createCharacterBase; _characterUtility = characterUtility; - _config = config; + Config = config; _createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState); } @@ -84,7 +84,7 @@ public sealed unsafe class MetaState : IDisposable, IService } public DecalReverter ResolveDecal(ResolveData resolve, bool which) - => new(_config, _characterUtility, _resources, resolve, which); + => new(Config, _characterUtility, _resources, resolve, which); public void Dispose() { @@ -99,7 +99,7 @@ public sealed unsafe class MetaState : IDisposable, IService _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, _lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); - var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection, + var decal = new DecalReverter(Config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); RspCollection.Push(_lastCreatedCollection); _characterBaseCreateMetaChanges.Dispose(); // Should always be empty. From f686a0ff09873a08eba20a332b27c0090834dbae Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 19 Jun 2024 20:37:08 +0000 Subject: [PATCH 192/865] [CI] Updating repo.json for testing_1.1.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c00e3c8f..03f13499 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.1.1.3", + "TestingAssemblyVersion": "1.1.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8cd8efa72347bda899a32baab1f878e55c3c7c9f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Jun 2024 14:24:35 +0200 Subject: [PATCH 193/865] Fix RSP scaling for NPC values. --- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index dcb3f19c..98e39061 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -29,6 +29,12 @@ public class RspHeightHook : FastHook, IDisposable && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { + // Special cases. + if (height == 0xFF) + return 1.0f; + if (height > 100) + height = 0; + var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) From ab1e11aba1b42a67552d5557cc16402b41f0e4ad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Jun 2024 14:51:17 +0200 Subject: [PATCH 194/865] Improve support info a bit. --- Penumbra/Penumbra.cs | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 905b998d..38d9c7b2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using OtterGui.Tasks; using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; +using System.Xml.Linq; namespace Penumbra; @@ -175,6 +176,26 @@ public class Penumbra : IDalamudPlugin _disposed = true; } + private void GatherRelevantPlugins(StringBuilder sb) + { + ReadOnlySpan relevantPlugins = + [ + "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", + ]; + var plugins = _services.GetService().InstalledPlugins + .GroupBy(p => p.InternalName) + .ToDictionary(g => g.Key, g => + { + var item = g.OrderByDescending(p => p.IsLoaded).ThenByDescending(p => p.Version).First(); + return (item.IsLoaded, item.Version, item.Name); + }); + foreach (var plugin in relevantPlugins) + { + if (plugins.TryGetValue(plugin, out var data)) + sb.Append($"> **`{data.Name + ':',-29}`** {data.Version}{(data.IsLoaded ? string.Empty : " (Disabled)")}\n"); + } + } + public string GatherSupportInformation() { var sb = new StringBuilder(10240); @@ -198,6 +219,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n"); sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n"); + GatherRelevantPlugins(sb); sb.AppendLine("**Mods**"); sb.Append($"> **`Installed Mods: `** {_modManager.Count}\n"); sb.Append($"> **`Mods with Config: `** {_modManager.Count(m => m.HasOptions)}\n"); @@ -212,27 +234,25 @@ public class Penumbra : IDalamudPlugin $"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n"); void PrintCollection(ModCollection c, CollectionCache _) - => sb.Append($"**Collection {c.AnonymizedName}**\n" - + $"> **`Inheritances: `** {c.DirectlyInheritsFrom.Count}\n" - + $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n" - + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); + => sb.Append( + $"> **`Collection {c.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); - sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); - sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); - sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); + sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); + sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); + sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); + sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { var collection = _collectionManager.Active.ByType(type); if (collection != null) - sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{name,-29}`** {collection.AnonymizedName}\n"); } foreach (var (name, id, collection) in _collectionManager.Active.Individuals.Assignments) - sb.Append($"> **`{id[0].Incognito(name) + ':',-30}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.AnonymizedName}\n"); foreach (var collection in _collectionManager.Caches.Active) PrintCollection(collection, collection._cache!); From 045abc787d46bb472366a08bf22c020906ec5690 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 20 Jun 2024 12:53:23 +0000 Subject: [PATCH 195/865] [CI] Updating repo.json for testing_1.1.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 03f13499..3142f8d4 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.1.1.4", + "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b07af32deead6ee2c601ff26d1d8a5194ea3ae30 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 22 Jun 2024 23:04:09 +0200 Subject: [PATCH 196/865] Fix doubled hook. --- .../Resources/ResourceHandleDestructor.cs | 4 +++ .../ResourceLoading/ResourceService.cs | 28 +--------------- .../UI/ResourceWatcher/ResourceWatcher.cs | 32 +++++++++++-------- 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 5ddb7eaa..ac3f504a 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -3,6 +3,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.Structs; +using Penumbra.UI.ResourceWatcher; namespace Penumbra.Interop.Hooks.Resources; @@ -15,6 +16,9 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr ShaderReplacementFixer, + + /// + ResourceWatcher, } public ResourceHandleDestructor(HookManager hooks) diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 54c86777..0947d2ec 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -26,7 +26,6 @@ public unsafe class ResourceService : IDisposable, IRequiredService interop.InitializeFromAttributes(this); _getResourceSyncHook.Enable(); _getResourceAsyncHook.Enable(); - _resourceHandleDestructorHook.Enable(); _incRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); @@ -51,7 +50,6 @@ public unsafe class ResourceService : IDisposable, IRequiredService { _getResourceSyncHook.Dispose(); _getResourceAsyncHook.Dispose(); - _resourceHandleDestructorHook.Dispose(); _incRefHook.Dispose(); _decRefHook.Dispose(); } @@ -67,8 +65,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService /// Whether to request the resource synchronously or asynchronously. /// The returned resource handle. If this is not null, calling original will be skipped. public delegate void GetResourcePreDelegate(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, - Utf8GamePath original, - GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue); /// /// Subscribers should be exception-safe. @@ -192,27 +189,4 @@ public unsafe class ResourceService : IDisposable, IRequiredService } #endregion - - #region Destructor - - /// Invoked before a resource handle is destructed. - /// The resource handle. - public delegate void ResourceHandleDtorDelegate(ResourceHandle* handle); - - /// - /// - /// Subscribers should be exception-safe. - /// - public event ResourceHandleDtorDelegate? ResourceHandleDestructor; - - [Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))] - private readonly Hook _resourceHandleDestructorHook = null!; - - private nint ResourceHandleDestructorDetour(ResourceHandle* handle) - { - ResourceHandleDestructor?.Invoke(handle); - return _resourceHandleDestructorHook.OriginalDisposeSafe(handle); - } - - #endregion } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index a7d1a8c6..3bf4cd88 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -8,8 +8,10 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -21,28 +23,30 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService public const int DefaultMaxEntries = 1024; public const RecordType AllRecords = RecordType.Request | RecordType.ResourceLoad | RecordType.FileLoad | RecordType.Destruction; - private readonly Configuration _config; - private readonly EphemeralConfig _ephemeral; - private readonly ResourceService _resources; - private readonly ResourceLoader _loader; - private readonly ActorManager _actors; - private readonly List _records = []; - private readonly ConcurrentQueue _newRecords = []; - private readonly ResourceWatcherTable _table; - private string _logFilter = string.Empty; - private Regex? _logRegex; - private int _newMaxEntries; + private readonly Configuration _config; + private readonly EphemeralConfig _ephemeral; + private readonly ResourceService _resources; + private readonly ResourceLoader _loader; + private readonly ResourceHandleDestructor _destructor; + private readonly ActorManager _actors; + private readonly List _records = []; + private readonly ConcurrentQueue _newRecords = []; + private readonly ResourceWatcherTable _table; + private string _logFilter = string.Empty; + private Regex? _logRegex; + private int _newMaxEntries; - public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader) + public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, ResourceHandleDestructor destructor) { _actors = actors; _config = config; _ephemeral = config.Ephemeral; _resources = resources; + _destructor = destructor; _loader = loader; _table = new ResourceWatcherTable(config.Ephemeral, _records); _resources.ResourceRequested += OnResourceRequested; - _resources.ResourceHandleDestructor += OnResourceDestroyed; + _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); _loader.ResourceLoaded += OnResourceLoaded; _loader.FileLoaded += OnFileLoaded; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); @@ -54,7 +58,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService Clear(); _records.TrimExcess(); _resources.ResourceRequested -= OnResourceRequested; - _resources.ResourceHandleDestructor -= OnResourceDestroyed; + _destructor.Unsubscribe(OnResourceDestroyed); _loader.ResourceLoaded -= OnResourceLoaded; _loader.FileLoaded -= OnFileLoaded; } From c2e74ed382c494272fd7ee1a7474ba33fb504c9b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 25 Jun 2024 22:50:52 +0200 Subject: [PATCH 197/865] Improve signatures. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/EstHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 3 ++- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 3 ++- 9 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3fbc7045..4b55c05c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3fbc704515b7b5fa9be02fb2a44719fc333747c1 +Subproject commit 4b55c05c72eb194bec0d28c52cf076962010424b diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index 583a2ac5..bfbe6866 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -15,7 +16,7 @@ public unsafe class EqdpAccessoryHook : FastHook, ID public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", "E8 ?? ?? ?? ?? 41 BF ?? ?? ?? ?? 83 FB", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index f5488f80..6ea38ee2 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -15,7 +16,7 @@ public unsafe class EqdpEquipHook : FastHook, IDisposabl public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", "E8 ?? ?? ?? ?? 85 DB 75 ?? F6 45", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index e96d8115..19b870b0 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -14,7 +15,7 @@ public unsafe class EqpHook : FastHook, IDisposable public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", "E8 ?? ?? ?? ?? 0F B6 44 24 ?? C0 E8", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index fa0bb3c5..3fc7080f 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -15,7 +16,7 @@ public class EstHook : FastHook, IDisposable public EstHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", "44 8B C9 83 EA ?? 74", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 44d35f12..72c8d075 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.Interop.PathResolving; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -16,7 +17,7 @@ public unsafe class GmpHook : FastHook, IDisposable public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", "E8 ?? ?? ?? ?? 48 85 C0 74 ?? 43 8D 0C", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index db22d90c..d1019d3e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Meta; @@ -19,7 +20,7 @@ public unsafe class RspBustHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index 98e39061..d54fe31e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Meta; @@ -18,7 +19,7 @@ public class RspHeightHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index 8d333575..8aa7ea9f 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -1,4 +1,5 @@ using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.PathResolving; using Penumbra.Meta; @@ -18,7 +19,7 @@ public class RspTailHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05", Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods); _metaState.Config.ModsEnabled += Toggle; } From 221b18751d092a8138d5e8087cf6d9143e47f25f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Jul 2024 17:08:27 +0200 Subject: [PATCH 198/865] Some updates. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Api/Api/RedrawApi.cs | 2 +- Penumbra/Api/Api/ResourceTreeApi.cs | 6 +- Penumbra/Api/IpcProviders.cs | 2 +- .../Api/IpcTester/CollectionsIpcTester.cs | 2 +- Penumbra/Api/IpcTester/EditingIpcTester.cs | 2 +- Penumbra/Api/IpcTester/GameStateIpcTester.cs | 7 +- Penumbra/Api/IpcTester/MetaIpcTester.cs | 2 +- .../Api/IpcTester/ModSettingsIpcTester.cs | 4 +- Penumbra/Api/IpcTester/ModsIpcTester.cs | 4 +- .../Api/IpcTester/PluginStateIpcTester.cs | 4 +- Penumbra/Api/IpcTester/RedrawingIpcTester.cs | 4 +- Penumbra/Api/IpcTester/ResolveIpcTester.cs | 2 +- .../Api/IpcTester/ResourceTreeIpcTester.cs | 2 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 2 +- Penumbra/Api/IpcTester/UiIpcTester.cs | 4 +- .../Manager/ActiveCollectionMigration.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 2 +- .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/IndividualCollections.Access.cs | 2 +- .../Manager/IndividualCollections.Files.cs | 2 +- .../Collections/Manager/InheritanceManager.cs | 2 +- Penumbra/Configuration.cs | 2 +- Penumbra/EphemeralConfig.cs | 2 +- Penumbra/Import/Models/HavokConverter.cs | 5 +- Penumbra/Import/Textures/BaseImage.cs | 2 +- Penumbra/Import/Textures/TexFileParser.cs | 16 ++-- Penumbra/Import/Textures/Texture.cs | 2 +- Penumbra/Import/Textures/TextureDrawer.cs | 2 +- Penumbra/Import/Textures/TextureManager.cs | 8 +- .../Animation/ApricotListenerSoundPlay.cs | 2 +- .../Animation/CharacterBaseLoadAnimation.cs | 2 +- Penumbra/Interop/Hooks/Animation/Dismount.cs | 2 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 2 +- .../Hooks/Animation/LoadCharacterSound.cs | 9 +- .../Hooks/Animation/LoadCharacterVfx.cs | 2 +- .../Hooks/Animation/LoadTimelineResources.cs | 2 +- .../Interop/Hooks/Animation/PlayFootstep.cs | 2 +- .../Hooks/Animation/ScheduleClipUpdate.cs | 2 +- .../Interop/Hooks/Animation/SomeActionLoad.cs | 10 +- .../Hooks/Animation/SomeMountAnimation.cs | 2 +- .../Interop/Hooks/Animation/SomePapLoad.cs | 2 +- .../Hooks/Animation/SomeParasolAnimation.cs | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 8 ++ .../Interop/Hooks/Meta/CalculateHeight.cs | 3 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 2 +- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 2 +- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 2 +- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 2 +- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 3 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 2 +- .../Interop/Hooks/Meta/RspSetupCharacter.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 2 +- Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 2 +- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- .../Interop/Hooks/Objects/CopyCharacter.cs | 6 +- .../Interop/Hooks/Objects/WeaponReload.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 26 ++--- .../PathResolving/CollectionResolver.cs | 4 +- .../Interop/PathResolving/CutsceneService.cs | 5 +- .../ResourceLoading/FileReadService.cs | 4 +- .../ResourceLoading/ResourceManagerService.cs | 33 ++----- .../ResourceLoading/ResourceService.cs | 2 +- .../Interop/ResourceLoading/TexMdlService.cs | 96 +++++++++++-------- .../Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- .../ResourceTree/ResourceTreeApiHelper.cs | 6 +- .../ResourceTree/ResourceTreeFactory.cs | 21 ++-- .../Interop/ResourceTree/TreeBuildCache.cs | 20 ++-- Penumbra/Interop/Services/FontReloader.cs | 4 +- Penumbra/Interop/Services/RedrawService.cs | 59 ++++++------ Penumbra/Interop/Structs/ClipScheduler.cs | 4 +- Penumbra/Interop/Structs/ResourceHandle.cs | 8 +- Penumbra/Meta/Files/MetaBaseFile.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 3 +- Penumbra/Mods/Manager/ModImportManager.cs | 2 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/Penumbra.cs | 4 +- Penumbra/Penumbra.csproj | 1 + Penumbra/Penumbra.json | 2 +- Penumbra/Services/DalamudConfigService.cs | 6 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/Services/MessageService.cs | 4 +- Penumbra/Services/StaticServiceManager.cs | 4 +- Penumbra/Services/ValidityChecker.cs | 12 +-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../ModEditWindow.Materials.Shpk.cs | 2 +- .../ModEditWindow.ShaderPackages.cs | 2 +- Penumbra/UI/ChangedItemDrawer.cs | 29 +++--- Penumbra/UI/CollectionTab/CollectionPanel.cs | 8 +- Penumbra/UI/ConfigWindow.cs | 4 +- Penumbra/UI/LaunchButton.cs | 25 ++--- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModPanel.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelHeader.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 11 +-- Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 4 +- Penumbra/UI/UiHelpers.cs | 2 +- Penumbra/UI/WindowSystem.cs | 4 +- repo.json | 2 +- 121 files changed, 338 insertions(+), 328 deletions(-) create mode 100644 Penumbra/Interop/Hooks/HookSettings.cs diff --git a/OtterGui b/OtterGui index 6fafc03b..437ef65c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6fafc03b34971be7c0e74fd9a638d1ed642ea19a +Subproject commit 437ef65c6464c54c8f40196dd2428da901d73aab diff --git a/Penumbra.Api b/Penumbra.Api index f1e4e520..43b0b47f 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f1e4e520daaa8f23e5c8b71d55e5992b8f6768e2 +Subproject commit 43b0b47f2d019af0fe4681dfc578f9232e3ba90c diff --git a/Penumbra.GameData b/Penumbra.GameData index 4b55c05c..3a97e5ae 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4b55c05c72eb194bec0d28c52cf076962010424b +Subproject commit 3a97e5aeee3b7375b333c1add5305d0ce80cbf83 diff --git a/Penumbra.String b/Penumbra.String index caa58c5c..f04abbab 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit caa58c5c92710e69ce07b9d736ebe2d228cb4488 +Subproject commit f04abbabedf5e757c5cbb970f3e513fef23e53cf diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index 03b42493..82d14f7b 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -13,7 +13,7 @@ public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiSe public void RedrawObject(string name, RedrawType setting) => redrawService.RedrawObject(name, setting); - public void RedrawObject(GameObject? gameObject, RedrawType setting) + public void RedrawObject(IGameObject? gameObject, RedrawType setting) => redrawService.RedrawObject(gameObject, setting); public void RedrawAll(RedrawType setting) diff --git a/Penumbra/Api/Api/ResourceTreeApi.cs b/Penumbra/Api/Api/ResourceTreeApi.cs index 6e9aaa48..dcec99bf 100644 --- a/Penumbra/Api/Api/ResourceTreeApi.cs +++ b/Penumbra/Api/Api/ResourceTreeApi.cs @@ -12,7 +12,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana { public Dictionary>?[] GetGameObjectResourcePaths(params ushort[] gameObjects) { - var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = resourceTreeFactory.FromCharacters(characters, 0); var pathDictionaries = ResourceTreeApiHelper.GetResourcePathDictionaries(resourceTrees); @@ -28,7 +28,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana public GameResourceDict?[] GetGameObjectResourcesOfType(ResourceType type, bool withUiData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionaries = ResourceTreeApiHelper.GetResourcesOfType(resourceTrees, type); @@ -45,7 +45,7 @@ public class ResourceTreeApi(ResourceTreeFactory resourceTreeFactory, ObjectMana public JObject?[] GetGameObjectResourceTrees(bool withUiData, params ushort[] gameObjects) { - var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); + var characters = gameObjects.Select(index => objects.GetDalamudObject((int)index)).OfType(); var resourceTrees = resourceTreeFactory.FromCharacters(characters, withUiData ? ResourceTreeFactory.Flags.WithUiData : 0); var resDictionary = ResourceTreeApiHelper.EncapsulateResourceTrees(resourceTrees); diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 6b146c39..861225fa 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -12,7 +12,7 @@ public sealed class IpcProviders : IDisposable, IApiService private readonly EventProvider _disposedProvider; private readonly EventProvider _initializedProvider; - public IpcProviders(DalamudPluginInterface pi, IPenumbraApi api) + public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api) { _disposedProvider = IpcSubscribers.Disposed.Provider(pi); _initializedProvider = IpcSubscribers.Initialized.Provider(pi); diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 2679bc69..026fabbc 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -13,7 +13,7 @@ using ImGuiClip = OtterGui.ImGuiClip; namespace Penumbra.Api.IpcTester; -public class CollectionsIpcTester(DalamudPluginInterface pi) : IUiService +public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService { private int _objectIdx; private string _collectionIdString = string.Empty; diff --git a/Penumbra/Api/IpcTester/EditingIpcTester.cs b/Penumbra/Api/IpcTester/EditingIpcTester.cs index 94b1e4e8..a1001630 100644 --- a/Penumbra/Api/IpcTester/EditingIpcTester.cs +++ b/Penumbra/Api/IpcTester/EditingIpcTester.cs @@ -8,7 +8,7 @@ using Penumbra.Api.IpcSubscribers; namespace Penumbra.Api.IpcTester; -public class EditingIpcTester(DalamudPluginInterface pi) : IUiService +public class EditingIpcTester(IDalamudPluginInterface pi) : IUiService { private string _inputPath = string.Empty; private string _inputPath2 = string.Empty; diff --git a/Penumbra/Api/IpcTester/GameStateIpcTester.cs b/Penumbra/Api/IpcTester/GameStateIpcTester.cs index 93806162..04541a57 100644 --- a/Penumbra/Api/IpcTester/GameStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/GameStateIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class GameStateIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber CharacterBaseCreating; public readonly EventSubscriber CharacterBaseCreated; public readonly EventSubscriber GameObjectResourcePathResolved; @@ -30,7 +30,7 @@ public class GameStateIpcTester : IUiService, IDisposable private int _currentCutsceneParent; private PenumbraApiEc _cutsceneError = PenumbraApiEc.Success; - public GameStateIpcTester(DalamudPluginInterface pi) + public GameStateIpcTester(IDalamudPluginInterface pi) { _pi = pi; CharacterBaseCreating = IpcSubscribers.CreatingCharacterBase.Subscriber(pi, UpdateLastCreated); @@ -134,7 +134,6 @@ public class GameStateIpcTester : IUiService, IDisposable private static unsafe string GetObjectName(nint gameObject) { var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject; - var name = obj != null ? obj->Name : null; - return name != null && *name != 0 ? new ByteString(name).ToString() : "Unknown"; + return obj != null && obj->Name[0] != 0 ? new ByteString(obj->Name).ToString() : "Unknown"; } } diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 3fa7de7f..8b393ade 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -6,7 +6,7 @@ using Penumbra.Api.IpcSubscribers; namespace Penumbra.Api.IpcTester; -public class MetaIpcTester(DalamudPluginInterface pi) : IUiService +public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService { private int _gameObjectIndex; diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index b117d603..23078576 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class ModSettingsIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber SettingChanged; private PenumbraApiEc _lastSettingsError = PenumbraApiEc.Success; @@ -33,7 +33,7 @@ public class ModSettingsIpcTester : IUiService, IDisposable private IReadOnlyDictionary? _availableSettings; private Dictionary>? _currentSettings; - public ModSettingsIpcTester(DalamudPluginInterface pi) + public ModSettingsIpcTester(IDalamudPluginInterface pi) { _pi = pi; SettingChanged = ModSettingChanged.Subscriber(pi, UpdateLastModSetting); diff --git a/Penumbra/Api/IpcTester/ModsIpcTester.cs b/Penumbra/Api/IpcTester/ModsIpcTester.cs index 2be51a80..a24861a3 100644 --- a/Penumbra/Api/IpcTester/ModsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModsIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class ModsIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; private string _modDirectory = string.Empty; private string _modName = string.Empty; @@ -38,7 +38,7 @@ public class ModsIpcTester : IUiService, IDisposable private string _lastMovedModFrom = string.Empty; private string _lastMovedModTo = string.Empty; - public ModsIpcTester(DalamudPluginInterface pi) + public ModsIpcTester(IDalamudPluginInterface pi) { _pi = pi; DeleteSubscriber = ModDeleted.Subscriber(pi, s => diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index 984f17b1..df82033d 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -12,7 +12,7 @@ namespace Penumbra.Api.IpcTester; public class PluginStateIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber ModDirectoryChanged; public readonly EventSubscriber Initialized; public readonly EventSubscriber Disposed; @@ -29,7 +29,7 @@ public class PluginStateIpcTester : IUiService, IDisposable private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; private bool? _lastEnabledValue; - public PluginStateIpcTester(DalamudPluginInterface pi) + public PluginStateIpcTester(IDalamudPluginInterface pi) { _pi = pi; ModDirectoryChanged = IpcSubscribers.ModDirectoryChanged.Subscriber(pi, UpdateModDirectoryChanged); diff --git a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs index 801f0b97..b862dde5 100644 --- a/Penumbra/Api/IpcTester/RedrawingIpcTester.cs +++ b/Penumbra/Api/IpcTester/RedrawingIpcTester.cs @@ -13,14 +13,14 @@ namespace Penumbra.Api.IpcTester; public class RedrawingIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; private readonly ObjectManager _objects; public readonly EventSubscriber Redrawn; private int _redrawIndex; private string _lastRedrawnString = "None"; - public RedrawingIpcTester(DalamudPluginInterface pi, ObjectManager objects) + public RedrawingIpcTester(IDalamudPluginInterface pi, ObjectManager objects) { _pi = pi; _objects = objects; diff --git a/Penumbra/Api/IpcTester/ResolveIpcTester.cs b/Penumbra/Api/IpcTester/ResolveIpcTester.cs index 978ed8d6..a79b099d 100644 --- a/Penumbra/Api/IpcTester/ResolveIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResolveIpcTester.cs @@ -7,7 +7,7 @@ using Penumbra.String.Classes; namespace Penumbra.Api.IpcTester; -public class ResolveIpcTester(DalamudPluginInterface pi) : IUiService +public class ResolveIpcTester(IDalamudPluginInterface pi) : IUiService { private string _currentResolvePath = string.Empty; private string _currentReversePath = string.Empty; diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs index 1f57fc9d..088a77bd 100644 --- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -15,7 +15,7 @@ using Penumbra.GameData.Structs; namespace Penumbra.Api.IpcTester; -public class ResourceTreeIpcTester(DalamudPluginInterface pi, ObjectManager objects) : IUiService +public class ResourceTreeIpcTester(IDalamudPluginInterface pi, ObjectManager objects) : IUiService { private readonly Stopwatch _stopwatch = new(); diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 0aa6821c..6d4f17b2 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -17,7 +17,7 @@ using Penumbra.Services; namespace Penumbra.Api.IpcTester; public class TemporaryIpcTester( - DalamudPluginInterface pi, + IDalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods, diff --git a/Penumbra/Api/IpcTester/UiIpcTester.cs b/Penumbra/Api/IpcTester/UiIpcTester.cs index a2c36938..647a4dda 100644 --- a/Penumbra/Api/IpcTester/UiIpcTester.cs +++ b/Penumbra/Api/IpcTester/UiIpcTester.cs @@ -10,7 +10,7 @@ namespace Penumbra.Api.IpcTester; public class UiIpcTester : IUiService, IDisposable { - private readonly DalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber PreSettingsTabBar; public readonly EventSubscriber PreSettingsPanel; public readonly EventSubscriber PostEnabled; @@ -28,7 +28,7 @@ public class UiIpcTester : IUiService, IDisposable private string _modName = string.Empty; private PenumbraApiEc _ec = PenumbraApiEc.Success; - public UiIpcTester(DalamudPluginInterface pi) + public UiIpcTester(IDalamudPluginInterface pi) { _pi = pi; PreSettingsTabBar = IpcSubscribers.PreSettingsTabBarDraw.Subscriber(pi, UpdateLastDrawnMod); diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 2f9e9b15..19f781fc 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 6d48f74b..60f9a427 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index cd680d36..a326fb92 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 785f0013..6b90a333 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -127,7 +127,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa } } - public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection) + public bool TryGetCollection(IGameObject? gameObject, out ModCollection? collection) => TryGetCollection(_actors.FromObject(gameObject, true, false, false), out collection); public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index 8a717b35..f7a26384 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -1,5 +1,5 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index f3482cdf..bc1a362c 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using OtterGui.Filesystem; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 7faed6a2..49aecfdc 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,5 +1,5 @@ using Dalamud.Configuration; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 52e276c7..7457c910 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index dc9d3e6a..e3797083 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -1,4 +1,7 @@ -using FFXIVClientStructs.Havok; +using FFXIVClientStructs.Havok.Common.Base.System.IO.OStream; +using FFXIVClientStructs.Havok.Common.Base.Types; +using FFXIVClientStructs.Havok.Common.Serialize.Resource; +using FFXIVClientStructs.Havok.Common.Serialize.Util; namespace Penumbra.Import.Models; diff --git a/Penumbra/Import/Textures/BaseImage.cs b/Penumbra/Import/Textures/BaseImage.cs index a4a0e203..eba2d8ba 100644 --- a/Penumbra/Import/Textures/BaseImage.cs +++ b/Penumbra/Import/Textures/BaseImage.cs @@ -103,7 +103,7 @@ public readonly struct BaseImage : IDisposable { null => 0, ScratchImage s => s.Meta.MipLevels, - TexFile t => t.Header.MipLevelsCount, + TexFile t => t.Header.MipCount, _ => 1, }; } diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 6f854022..09025b61 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -79,8 +79,8 @@ public static class TexFileParser w.Write(header.Width); w.Write(header.Height); w.Write(header.Depth); - w.Write(header.MipLevelsCount); - w.Write((byte)0); // TODO Lumina Update + w.Write(header.MipCount); + w.Write(header.MipUnknownFlag); // TODO Lumina Update unsafe { w.Write(header.LodOffset[0]); @@ -96,11 +96,11 @@ public static class TexFileParser var meta = scratch.Meta; var ret = new TexFile.TexHeader() { - Height = (ushort)meta.Height, - Width = (ushort)meta.Width, - Depth = (ushort)Math.Max(meta.Depth, 1), - MipLevelsCount = (byte)Math.Min(meta.MipLevels, 13), - Format = meta.Format.ToTexFormat(), + Height = (ushort)meta.Height, + Width = (ushort)meta.Width, + Depth = (ushort)Math.Max(meta.Depth, 1), + MipCount = (byte)Math.Min(meta.MipLevels, 13), + Format = meta.Format.ToTexFormat(), Type = meta.Dimension switch { _ when meta.IsCubeMap => TexFile.Attribute.TextureTypeCube, @@ -143,7 +143,7 @@ public static class TexFileParser Height = header.Height, Width = header.Width, Depth = Math.Max(header.Depth, (ushort)1), - MipLevels = header.MipLevelsCount, + MipLevels = header.MipCount, ArraySize = 1, Format = header.Format.ToDXGI(), Dimension = header.Type.ToDimension(), diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c4d6dc56..c5207e94 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; using OtterTex; namespace Penumbra.Import.Textures; diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index 427db92d..bd95d1ab 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -105,7 +105,7 @@ public static class TextureDrawer ImGuiUtil.DrawTableColumn("Format"); ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(t.Header.MipLevelsCount.ToString()); + ImGuiUtil.DrawTableColumn(t.Header.MipCount.ToString()); ImGuiUtil.DrawTableColumn("Data Size"); ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); break; diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 4aa64209..cc785d02 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,5 +1,5 @@ -using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Lumina.Data.Files; using OtterGui.Log; @@ -13,7 +13,7 @@ using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, Logger logger) +public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider) : SingleTaskQueue, IDisposable, IService { private readonly Logger _logger = logger; @@ -211,7 +211,7 @@ public sealed class TextureManager(UiBuilder uiBuilder, IDataManager gameData, L /// Load a texture wrap for a given image. public IDalamudTextureWrap LoadTextureWrap(byte[] rgba, int width, int height) - => uiBuilder.LoadImageRaw(rgba, width, height, 4); + => textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(width, height), rgba, "Penumbra.Texture"); /// Load any supported file from game data or drive depending on extension and if the path is rooted. public (BaseImage, TextureType) Load(string path) diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 77927593..e58c7268 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -21,7 +21,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true); + Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); diff --git a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs index df25358e..f99d8ca4 100644 --- a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs @@ -26,7 +26,7 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true); + Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(DrawObject* drawBase); diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs index 8085bcdb..523e750c 100644 --- a/Penumbra/Interop/Hooks/Animation/Dismount.cs +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -15,7 +15,7 @@ public sealed unsafe class Dismount : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, true); + Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(nint a1, nint a2); diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index 1a78d3b4..0f51157c 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -20,7 +20,7 @@ public sealed unsafe class LoadAreaVfx : FastHook _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, true); + Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index 98454a77..ed04880e 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.CrashHandler.Buffers; @@ -15,12 +16,10 @@ public sealed unsafe class LoadCharacterSound : FastHook("Load Character Sound", - (nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, - true); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index 77aaa742..af801345 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -26,7 +26,7 @@ public sealed unsafe class LoadCharacterVfx : FastHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true); + Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, HookSettings.VfxIdentificationHooks); } public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 018892a0..4e9037bd 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -30,7 +30,7 @@ public sealed unsafe class LoadTimelineResources : FastHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true); + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); } public delegate ulong Delegate(nint timeline); diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs index 491d7662..e4a8c83c 100644 --- a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -14,7 +14,7 @@ public sealed unsafe class PlayFootstep : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, true); + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(GameObject* gameObject, int id, int unk); diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs index 342ffc25..645b3565 100644 --- a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -23,7 +23,7 @@ public sealed unsafe class ScheduleClipUpdate : FastHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true); + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(ClipScheduler* x); diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs index 6de3aeb0..1f3c0e3b 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.CrashHandler.Buffers; @@ -20,15 +20,15 @@ public sealed unsafe class SomeActionLoad : FastHook _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, true); + Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, HookSettings.VfxIdentificationHooks); } - public delegate void Delegate(ActionTimelineManager* timelineManager); + public delegate void Delegate(TimelineContainer* timelineManager); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Detour(ActionTimelineManager* timelineManager) + private void Detour(TimelineContainer* timelineManager) { - var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true); + var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->OwnerObject, true); var last = _state.SetAnimationData(newData); Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}."); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ActionLoad); diff --git a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs index 5dd8227d..f2b48afe 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeMountAnimation : FastHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, true); + Task = hooks.CreateHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index fad1f819..8f952df5 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -22,7 +22,7 @@ public sealed unsafe class SomePapLoad : FastHook _collectionResolver = collectionResolver; _objects = objects; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, true); + Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(nint a1, int a2, nint a3, int a4); diff --git a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs index ab4a7201..165bd5eb 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeParasolAnimation : FastHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, true); + Task = hooks.CreateHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, HookSettings.VfxIdentificationHooks); } public delegate void Delegate(DrawObject* drawObject, int unk1); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs new file mode 100644 index 00000000..6d511a28 --- /dev/null +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -0,0 +1,8 @@ +namespace Penumbra.Interop.Hooks; + +public static class HookSettings +{ + public const bool MetaEntryHooks = false; + public const bool MetaParentHooks = false; + public const bool VfxIdentificationHooks = false; +} diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 7936b831..aab64871 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.Interop.PathResolving; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; @@ -15,7 +14,7 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, true); + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, HookSettings.MetaParentHooks); } public delegate ulong Delegate(Character* character); diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index f589cf4e..f69e98e7 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, true); + Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, HookSettings.MetaParentHooks); } public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index bfbe6866..63cca53f 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -16,7 +16,7 @@ public unsafe class EqdpAccessoryHook : FastHook, ID public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 6ea38ee2..5d5d2f84 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -16,7 +16,7 @@ public unsafe class EqdpEquipHook : FastHook, IDisposabl public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index 19b870b0..f47db795 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -15,7 +15,7 @@ public unsafe class EqpHook : FastHook, IDisposable public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 3fc7080f..5b272019 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -16,7 +16,7 @@ public class EstHook : FastHook, IDisposable public EstHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs index a10b511a..8bd49500 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs @@ -15,7 +15,7 @@ public sealed unsafe class GetEqpIndirect : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, true); + Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 30ec2597..3767c4a2 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -15,7 +15,7 @@ public sealed unsafe class GetEqpIndirect2 : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, true); + Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 72c8d075..329a8beb 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -17,7 +17,7 @@ public unsafe class GmpHook : FastHook, IDisposable public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 2c17362d..79e7f6a6 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; @@ -14,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, true); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index d1019d3e..eb8a8a37 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -20,7 +20,7 @@ public unsafe class RspBustHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index d54fe31e..f8f9e51e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -19,7 +19,7 @@ public class RspHeightHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 58856f52..8bcc7593 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class RspSetupCharacter : FastHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, true); + Task = hooks.CreateHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5); diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index 8aa7ea9f..86d21c6f 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -19,7 +19,7 @@ public class RspTailHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods); + Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index 82b24dc4..83c0e0c4 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -19,7 +19,7 @@ public sealed unsafe class SetupVisor : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, true); + Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, HookSettings.MetaParentHooks); } public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index 76854bca..a088a0f2 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -15,7 +15,7 @@ public sealed unsafe class UpdateModel : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, true); + Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 7b730f84..20c96f56 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -20,7 +20,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr> _task; public nint Address - => (nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter; + => (nint)CharacterSetupContainer.MemberFunctionPointers.CopyFromCharacter; public void Enable() => _task.Result.Enable(); @@ -34,9 +34,9 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr _task.IsCompletedSuccessfully; - private delegate ulong Delegate(CharacterSetup* target, Character* source, uint unk); + private delegate ulong Delegate(CharacterSetupContainer* target, Character* source, uint unk); - private ulong Detour(CharacterSetup* target, Character* source, uint unk) + private ulong Detour(CharacterSetupContainer* target, Character* source, uint unk) { // TODO: update when CS updated. var character = ((Character**)target)[1]; diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index 31c6b883..fec0a13f 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -39,7 +39,7 @@ public sealed unsafe class WeaponReload : EventWrapperPtrParent; + var gameObject = drawData->OwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}."); Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon)); _task.Result.Original(drawData, slot, weapon, d, e, f, g); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 5941773f..a7e82b72 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -44,18 +44,20 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable { _parent = parent; // @formatter:off - _resolveDecalPathHook = Create($"{name}.{nameof(ResolveDecal)}", hooks, vTable[83], ResolveDecal); - _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[85], ResolveEid); - _resolveImcPathHook = Create($"{name}.{nameof(ResolveImc)}", hooks, vTable[81], ResolveImc); - _resolveMPapPathHook = Create( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[79], ResolveMPap); - _resolveMdlPathHook = Create($"{name}.{nameof(ResolveMdl)}", hooks, vTable[73], type, ResolveMdl, ResolveMdlHuman); - _resolveMtrlPathHook = Create( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[82], ResolveMtrl); - _resolvePapPathHook = Create( $"{name}.{nameof(ResolvePap)}", hooks, vTable[76], type, ResolvePap, ResolvePapHuman); - _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[75], type, ResolvePhyb, ResolvePhybHuman); - _resolveSklbPathHook = Create($"{name}.{nameof(ResolveSklb)}", hooks, vTable[72], type, ResolveSklb, ResolveSklbHuman); - _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[74], type, ResolveSkp, ResolveSkpHuman); - _resolveTmbPathHook = Create( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[77], ResolveTmb); - _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[84], type, ResolveVfx, ResolveVfxHuman); + _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); + + + _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); // @formatter:on Enable(); } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index bc474952..136da0f5 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -120,7 +120,7 @@ public sealed unsafe class CollectionResolver( var lobby = AgentLobby.Instance(); if (lobby != null) { - var span = lobby->LobbyData.CharaSelectEntries.Span; + var span = lobby->LobbyData.CharaSelectEntries.AsSpan(); // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; if (idx >= 0 && idx < span.Length && span[idx].Value != null) @@ -148,7 +148,7 @@ public sealed unsafe class CollectionResolver( /// Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. private bool Aesthetician(GameObject* gameObject, out ResolveData ret) { - if (gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero) + if (gameGui.GetAddonByName("ScreenLog") != nint.Zero) { ret = ResolveData.Invalid; return false; diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index feb27341..8e32dd76 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; @@ -19,7 +20,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable private readonly CharacterDestructor _characterDestructor; private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); - public IEnumerable> Actors + public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) .Where(i => _objects[i].Valid) .Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!)); @@ -42,7 +43,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable /// Does not check for valid input index. /// Returns null if no connected actor is set or the actor does not exist anymore. /// - private Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] + private IGameObject? this[int idx] { get { diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/ResourceLoading/FileReadService.cs index f1d7fe24..5edba790 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/ResourceLoading/FileReadService.cs @@ -63,7 +63,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService byte? ret = null; _lastFileThreadResourceManager.Value = resourceManager; ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret); - _lastFileThreadResourceManager.Value = IntPtr.Zero; + _lastFileThreadResourceManager.Value = nint.Zero; return ret ?? _readSqPackHook.Original(resourceManager, fileDescriptor, priority, isSync); } @@ -82,7 +82,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService /// So we keep track of them per thread and use them. /// private nint GetResourceManager() - => !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == IntPtr.Zero + => !_lastFileThreadResourceManager.IsValueCreated || _lastFileThreadResourceManager.Value == nint.Zero ? (nint)_resourceManager.ResourceManager : _lastFileThreadResourceManager.Value; } diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs index c885c317..0479d2a6 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs @@ -25,8 +25,8 @@ public unsafe class ResourceManagerService : IRequiredService ref var manager = ref *ResourceManager; var catIdx = (uint)cat >> 0x18; cat = (ResourceCategory)(ushort)cat; - ref var category = ref manager.ResourceGraph->ContainerArraySpan[(int)cat]; - var extMap = FindInMap(category.CategoryMapsSpan[(int)catIdx].Value, (uint)ext); + ref var category = ref manager.ResourceGraph->Containers[(int)cat]; + var extMap = FindInMap(category.CategoryMaps[(int)catIdx].Value, (uint)ext); if (extMap == null) return null; @@ -44,10 +44,10 @@ public unsafe class ResourceManagerService : IRequiredService ref var manager = ref *ResourceManager; foreach (var resourceType in Enum.GetValues().SkipLast(1)) { - ref var graph = ref manager.ResourceGraph->ContainerArraySpan[(int)resourceType]; + ref var graph = ref manager.ResourceGraph->Containers[(int)resourceType]; for (var i = 0; i < 20; ++i) { - var map = graph.CategoryMapsSpan[i]; + var map = graph.CategoryMaps[i]; if (map.Value != null) action(resourceType, map, i); } @@ -79,25 +79,10 @@ public unsafe class ResourceManagerService : IRequiredService where TKey : unmanaged, IComparable where TValue : unmanaged { - if (map == null || map->Count == 0) + if (map == null) return null; - var node = map->Head->Parent; - while (!node->IsNil) - { - switch (key.CompareTo(node->KeyValuePair.Item1)) - { - case 0: return &node->KeyValuePair.Item2; - case < 0: - node = node->Left; - break; - default: - node = node->Right; - break; - } - } - - return null; + return map->TryGetValuePointer(key, out var val) ? val : null; } // Iterate in tree-order through a map, applying action to each KeyValuePair. @@ -105,10 +90,10 @@ public unsafe class ResourceManagerService : IRequiredService where TKey : unmanaged where TValue : unmanaged { - if (map == null || map->Count == 0) + if (map == null) return; - for (var node = map->SmallestValue; !node->IsNil; node = node->Next()) - action(node->KeyValuePair.Item1, node->KeyValuePair.Item2); + foreach (var (key, value) in *map) + action(key, value); } } diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/ResourceLoading/ResourceService.cs index 0947d2ec..d623d72d 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/ResourceLoading/ResourceService.cs @@ -127,7 +127,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService #endregion - private delegate IntPtr ResourceHandlePrototype(ResourceHandle* handle); + private delegate nint ResourceHandlePrototype(ResourceHandle* handle); #region IncRef diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/ResourceLoading/TexMdlService.cs index e617673e..a2b43c64 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/ResourceLoading/TexMdlService.cs @@ -1,6 +1,7 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.LayoutEngine; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Services; using Penumbra.Api.Enums; @@ -12,88 +13,99 @@ namespace Penumbra.Interop.ResourceLoading; public unsafe class TexMdlService : IDisposable, IRequiredService { /// Custom ulong flag to signal our files as opposed to SE files. - public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); - + public static readonly nint CustomFileFlag = new(0xDEADBEEF); + /// /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// public IReadOnlySet CustomFileCrc => _customFileCrc; - + public TexMdlService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); - _checkFileStateHook.Enable(); - _loadTexFileExternHook.Enable(); - _loadMdlFileExternHook.Enable(); + //_checkFileStateHook.Enable(); + //_loadTexFileExternHook.Enable(); + //_loadMdlFileExternHook.Enable(); } - + /// Add CRC64 if the given file is a model or texture file and has an associated path. public void AddCrc(ResourceType type, FullPath? path) { if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex) _customFileCrc.Add(path.Value.Crc64); } - + /// Add a fixed CRC64 value. public void AddCrc(ulong crc64) => _customFileCrc.Add(crc64); - + public void Dispose() { - _checkFileStateHook.Dispose(); - _loadTexFileExternHook.Dispose(); - _loadMdlFileExternHook.Dispose(); + //_checkFileStateHook.Dispose(); + //_loadTexFileExternHook.Dispose(); + //_loadMdlFileExternHook.Dispose(); } - + private readonly HashSet _customFileCrc = new(); - - private delegate IntPtr CheckFileStatePrototype(IntPtr unk1, ulong crc64); - + + private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); + [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] private readonly Hook _checkFileStateHook = null!; - + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. /// - private IntPtr CheckFileStateDetour(IntPtr ptr, ulong crc64) + private nint CheckFileStateDetour(nint ptr, ulong crc64) => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); - - - private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3); - + + + private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, nint unk2, bool unk3); + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadTexFileLocal)] private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!; - - private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2); - + + private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, nint unk1, bool unk2); + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadMdlFileLocal)] private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; - - - private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4); - - [Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] - private readonly Hook _loadTexFileExternHook = null!; - + + + private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, nint unk2, bool unk3, nint unk4); + + private delegate byte TexResourceHandleVf32Prototype(ResourceHandle* handle, nint unk1, byte unk2); + + //[Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] + //private readonly Hook _vf32Hook = null!; + // + //private byte Vf32Detour(ResourceHandle* handle, nint unk1, byte unk2) + //{ + // var ret = _vf32Hook.Original(handle, unk1, unk2); + // return _loadTexFileLocal() + //} + + //[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] + //private readonly Hook _loadTexFileExternHook = null!; + /// We hook the extern functions to just return the local one if given the custom flag as last argument. - private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr) - => ptr.Equals(CustomFileFlag) - ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) - : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); - - public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, IntPtr unk1, bool unk2, IntPtr unk3); - - + //private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, nint unk2, bool unk3, nint ptr) + // => ptr.Equals(CustomFileFlag) + // ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) + // : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); + + public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); + + [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] private readonly Hook _loadMdlFileExternHook = null!; - + /// We hook the extern functions to just return the local one if given the custom flag as last argument. - private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr) + private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, nint unk1, bool unk2, nint ptr) => ptr.Equals(CustomFileFlag) ? _loadMdlFileLocal.Invoke(resourceHandle, unk1, unk2) : _loadMdlFileExternHook.Original(resourceHandle, unk1, unk2, ptr); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index e38bf4f6..ca8836b0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -111,7 +111,7 @@ internal unsafe partial record ResolveContext( if (resourceHandle == null) throw new ArgumentNullException(nameof(resourceHandle)); - var fileName = resourceHandle->FileName.AsSpan(); + var fileName = (ReadOnlySpan) resourceHandle->FileName.AsSpan(); var additionalData = ByteString.Empty; if (PathDataHandler.Split(fileName, out fileName, out var data)) additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index fc8c805a..b8bad84a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -62,7 +62,7 @@ public class ResourceTree var equipment = modelType switch { CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(&character->DrawData.Head, 10), + CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), _ => ReadOnlySpan.Empty, }; ModelId = character->CharacterData.ModelCharaId; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 0d1e3abc..22025dd6 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.ResourceTree; internal static class ResourceTreeApiHelper { public static Dictionary>> GetResourcePathDictionaries( - IEnumerable<(Character, ResourceTree)> resourceTrees) + IEnumerable<(ICharacter, ResourceTree)> resourceTrees) { var pathDictionaries = new Dictionary>>(4); @@ -47,7 +47,7 @@ internal static class ResourceTreeApiHelper } } - public static Dictionary GetResourcesOfType(IEnumerable<(Character, ResourceTree)> resourceTrees, + public static Dictionary GetResourcesOfType(IEnumerable<(ICharacter, ResourceTree)> resourceTrees, ResourceType type) { var resDictionaries = new Dictionary(4); @@ -74,7 +74,7 @@ internal static class ResourceTreeApiHelper return resDictionaries; } - public static Dictionary EncapsulateResourceTrees(IEnumerable<(Character, ResourceTree)> resourceTrees) + public static Dictionary EncapsulateResourceTrees(IEnumerable<(ICharacter, ResourceTree)> resourceTrees) { var resDictionary = new Dictionary(4); foreach (var (gameObject, resourceTree) in resourceTrees) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index e26c1436..1f6d1f6f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; @@ -23,13 +24,13 @@ public class ResourceTreeFactory( private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); - public IEnumerable GetLocalPlayerRelatedCharacters() + public IEnumerable GetLocalPlayerRelatedCharacters() { var cache = CreateTreeBuildCache(); return cache.GetLocalPlayerRelatedCharacters(); } - public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable( + public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromObjectTable( Flags flags) { var cache = CreateTreeBuildCache(); @@ -43,8 +44,8 @@ public class ResourceTreeFactory( } } - public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters( - IEnumerable characters, Flags flags) + public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromCharacters( + IEnumerable characters, Flags flags) { var cache = CreateTreeBuildCache(); foreach (var character in characters) @@ -55,10 +56,10 @@ public class ResourceTreeFactory( } } - public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, Flags flags) + public ResourceTree? FromCharacter(ICharacter character, Flags flags) => FromCharacter(character, CreateTreeBuildCache(), flags); - private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, Flags flags) + private unsafe ResourceTree? FromCharacter(ICharacter character, TreeBuildCache cache, Flags flags) { if (!character.IsValid()) return null; @@ -74,7 +75,7 @@ public class ResourceTreeFactory( var localPlayerRelated = cache.IsLocalPlayerRelated(character); var (name, anonymizedName, related) = GetCharacterName(character); - var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId; + var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, @@ -155,14 +156,14 @@ public class ResourceTreeFactory( } } - private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character) + private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(ICharacter character) { var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); var identifierStr = identifier.ToString(); return (identifierStr, identifier.Incognito(identifierStr), IsPlayerRelated(identifier, owner)); } - private unsafe bool IsPlayerRelated(Dalamud.Game.ClientState.Objects.Types.Character? character) + private unsafe bool IsPlayerRelated(ICharacter? character) { if (character == null) return false; @@ -175,7 +176,7 @@ public class ResourceTreeFactory( => identifier.Type switch { IdentifierType.Player => true, - IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as Dalamud.Game.ClientState.Objects.Types.Character), + IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as ICharacter), _ => false, }; diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 2798002a..ca5ff736 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -13,7 +13,7 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data { private readonly Dictionary _shaderPackages = []; - public unsafe bool IsLocalPlayerRelated(Character character) + public unsafe bool IsLocalPlayerRelated(ICharacter character) { var player = objects.GetDalamudObject(0); if (player == null) @@ -25,36 +25,36 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data return actualIndex switch { < 2 => true, - < (int)ScreenActor.CutsceneStart => gameObject->OwnerID == player.ObjectId, + < (int)ScreenActor.CutsceneStart => gameObject->OwnerId == player.EntityId, _ => false, }; } - public IEnumerable GetCharacters() - => objects.Objects.OfType(); + public IEnumerable GetCharacters() + => objects.Objects.OfType(); - public IEnumerable GetLocalPlayerRelatedCharacters() + public IEnumerable GetLocalPlayerRelatedCharacters() { var player = objects.GetDalamudObject(0); if (player == null) yield break; - yield return (Character)player; + yield return (ICharacter)player; var minion = objects.GetDalamudObject(1); if (minion != null) - yield return (Character)minion; + yield return (ICharacter)minion; - var playerId = player.ObjectId; + var playerId = player.EntityId; for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) { - if (objects.GetDalamudObject(i) is Character owned && owned.OwnerId == playerId) + if (objects.GetDalamudObject(i) is ICharacter owned && owned.OwnerId == playerId) yield return owned; } for (var i = ObjectIndex.CutsceneStart.Index; i < ObjectIndex.CharacterScreen.Index; ++i) { - var character = objects.GetDalamudObject((int) i) as Character; + var character = objects.GetDalamudObject((int) i) as ICharacter; if (character == null) continue; diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 259fdd10..4f48f08f 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -34,7 +34,7 @@ public unsafe class FontReloader : IService if (framework == null) return; - var uiModule = framework->GetUiModule(); + var uiModule = framework->GetUIModule(); if (uiModule == null) return; @@ -43,7 +43,7 @@ public unsafe class FontReloader : IService return; _atkModule = &atkModule->AtkModule; - _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->vtbl)[Offsets.ReloadFontsVfunc]; + _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->VirtualTable)[Offsets.ReloadFontsVfunc]; }); } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 61d7b90c..163b2c0e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -4,8 +4,8 @@ using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Housing; -using FFXIVClientStructs.Interop; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; @@ -37,11 +37,11 @@ public unsafe partial class RedrawService : IService => _clientState.IsGPosing; // VFuncs that disable and enable draw, used only for GPose actors. - private static void DisableDraw(GameObject actor) - => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); + private static void DisableDraw(IGameObject actor) + => ((delegate* unmanaged**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); - private static void EnableDraw(GameObject actor) - => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); + private static void EnableDraw(IGameObject actor) + => ((delegate* unmanaged**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); // Check whether we currently are in GPose. // Also clear the name list. @@ -57,7 +57,7 @@ public unsafe partial class RedrawService : IService // obj will be the object itself (or null) and false will be returned. // If we are in GPose and a game object with the same name as the original actor is found, // this will be in obj and true will be returned. - private bool FindCorrectActor(int idx, out GameObject? obj) + private bool FindCorrectActor(int idx, out IGameObject? obj) { obj = _objects.GetDalamudObject(idx); if (!InGPose || obj == null || IsGPoseActor(idx)) @@ -91,11 +91,11 @@ public unsafe partial class RedrawService : IService } } - return obj; + return false; } // Do not ever redraw any of the five UI Window actors. - private static bool BadRedrawIndices(GameObject? actor, out int tableIndex) + private static bool BadRedrawIndices(IGameObject? actor, out int tableIndex) { if (actor == null) { @@ -155,13 +155,13 @@ public sealed unsafe partial class RedrawService : IDisposable _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } - public static DrawState* ActorDrawState(GameObject actor) + public static DrawState* ActorDrawState(IGameObject actor) => (DrawState*)(&((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->RenderFlags); - private static int ObjectTableIndex(GameObject actor) + private static int ObjectTableIndex(IGameObject actor) => ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->ObjectIndex; - private void WriteInvisible(GameObject? actor) + private void WriteInvisible(IGameObject? actor) { if (BadRedrawIndices(actor, out var tableIndex)) return; @@ -172,7 +172,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) DisableDraw(actor!); - if (actor is PlayerCharacter + if (actor is IPlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) |= DrawState.Invisibility; @@ -181,7 +181,7 @@ public sealed unsafe partial class RedrawService : IDisposable } } - private void WriteVisible(GameObject? actor) + private void WriteVisible(IGameObject? actor) { if (BadRedrawIndices(actor, out var tableIndex)) return; @@ -192,7 +192,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (gPose) EnableDraw(actor!); - if (actor is PlayerCharacter + if (actor is IPlayerCharacter && _objects.GetDalamudObject(tableIndex + 1) is { ObjectKind: ObjectKind.MountType or ObjectKind.Ornament } mountOrOrnament) { *ActorDrawState(mountOrOrnament) &= ~DrawState.Invisibility; @@ -203,7 +203,7 @@ public sealed unsafe partial class RedrawService : IDisposable GameObjectRedrawn?.Invoke(actor!.Address, tableIndex); } - private void ReloadActor(GameObject? actor) + private void ReloadActor(IGameObject? actor) { if (BadRedrawIndices(actor, out var tableIndex)) return; @@ -214,7 +214,7 @@ public sealed unsafe partial class RedrawService : IDisposable _queue.Add(~tableIndex); } - private void ReloadActorAfterGPose(GameObject? actor) + private void ReloadActorAfterGPose(IGameObject? actor) { if (_objects[GPosePlayerIdx].Valid) { @@ -284,21 +284,21 @@ public sealed unsafe partial class RedrawService : IDisposable _queue.RemoveRange(numKept, _queue.Count - numKept); } - private static uint GetCurrentAnimationId(GameObject obj) + private static uint GetCurrentAnimationId(IGameObject obj) { var gameObj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address; if (gameObj == null || !gameObj->IsCharacter()) return 0; var chara = (Character*)gameObj; - var ptr = (byte*)&chara->ActionTimelineManager + 0xF0; + var ptr = (byte*)&chara->Timeline + 0xF0; return *(uint*)ptr; } - private static bool DelayRedraw(GameObject obj) + private static bool DelayRedraw(IGameObject obj) => ((Character*)obj.Address)->Mode switch { - (Character.CharacterModes)6 => // fishing + (CharacterModes)6 => // fishing GetCurrentAnimationId(obj) switch { 278 => true, // line out. @@ -345,7 +345,7 @@ public sealed unsafe partial class RedrawService : IDisposable HandleTarget(); } - public void RedrawObject(GameObject? actor, RedrawType settings) + public void RedrawObject(IGameObject? actor, RedrawType settings) { switch (settings) { @@ -359,13 +359,13 @@ public sealed unsafe partial class RedrawService : IDisposable } } - private GameObject? GetLocalPlayer() + private IGameObject? GetLocalPlayer() { var gPosePlayer = _objects.GetDalamudObject(GPosePlayerIdx); return gPosePlayer ?? _objects.GetDalamudObject(0); } - public bool GetName(string lowerName, out GameObject? actor) + public bool GetName(string lowerName, out IGameObject? actor) { (actor, var ret) = lowerName switch { @@ -419,15 +419,14 @@ public sealed unsafe partial class RedrawService : IDisposable if (housingManager == null) return; - var currentTerritory = housingManager->CurrentTerritory; - if (currentTerritory == null) - return; - if (!housingManager->IsInside()) + var currentTerritory = (OutdoorTerritory*) housingManager->CurrentTerritory; + if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Outdoor) return; - foreach (var f in currentTerritory->FurnitureSpan.PointerEnumerator()) + + foreach (ref var f in currentTerritory->Furniture) { - var gameObject = f->Index >= 0 ? currentTerritory->HousingObjectManager.ObjectsSpan[f->Index].Value : null; + var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null; if (gameObject == null) continue; diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs index 3211c4f9..44a905b8 100644 --- a/Penumbra/Interop/Structs/ClipScheduler.cs +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -4,8 +4,8 @@ namespace Penumbra.Interop.Structs; public unsafe struct ClipScheduler { [FieldOffset(0)] - public IntPtr* VTable; + public nint* VTable; [FieldOffset(0x38)] - public IntPtr SchedulerTimeline; + public nint SchedulerTimeline; } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 382368b4..058b9004 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -83,12 +83,12 @@ public unsafe struct ResourceHandle [FieldOffset(0xB8)] public uint DataLength; - public (IntPtr Data, int Length) GetData() + public (nint Data, int Length) GetData() => Data != null - ? ((IntPtr)Data->DataPtr, (int)Data->DataLength) - : (IntPtr.Zero, 0); + ? ((nint)Data->DataPtr, (int)Data->DataLength) + : (nint.Zero, 0); - public bool SetData(IntPtr data, int length) + public bool SetData(nint data, int length) { if (Data == null) return false; diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 86a55101..5bc36068 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -71,7 +71,7 @@ public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, public int Length { get; private set; } public CharacterUtility.InternalIndex Index { get; } = CharacterUtility.ReverseIndices[(int)idx]; - protected (IntPtr Data, int Length) DefaultData + protected (nint Data, int Length) DefaultData => Manager.CharacterUtility.DefaultResource(Index); /// Reset to default values. diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 9d31664b..b059813b 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Utility; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index c6bc4939..c0876f5d 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using OtterGui.Services; diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index c52828c0..46204d6c 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 220d0a7c..95f49230 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -1,10 +1,9 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; -using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index ff39b021..39a53bb9 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Services; using Penumbra.Import; diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 594ec9d2..712630c6 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0e66367a..546f5f5c 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 38d9c7b2..b1ad0b78 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -48,7 +48,7 @@ public class Penumbra : IDalamudPlugin private readonly ServiceManager _services; - public Penumbra(DalamudPluginInterface pluginInterface) + public Penumbra(IDalamudPluginInterface pluginInterface) { try { @@ -182,7 +182,7 @@ public class Penumbra : IDalamudPlugin [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", ]; - var plugins = _services.GetService().InstalledPlugins + var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) .ToDictionary(g => g.Key, g => { diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2e53bd22..ed5c5e30 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -37,6 +37,7 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\ + H:\Projects\FFPlugins\Dalamud\bin\Release\ diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 85e01c84..805f4d85 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "LoadPriority": 69420, "LoadState": 2, "LoadSync": true, diff --git a/Penumbra/Services/DalamudConfigService.cs b/Penumbra/Services/DalamudConfigService.cs index 8379a3e7..012a45f5 100644 --- a/Penumbra/Services/DalamudConfigService.cs +++ b/Penumbra/Services/DalamudConfigService.cs @@ -10,9 +10,9 @@ public class DalamudConfigService : IService try { var serviceType = - typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); - var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); - var interfaceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); + typeof(IDalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); + var configType = typeof(IDalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); + var interfaceType = typeof(IDalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); if (serviceType == null || configType == null || interfaceType == null) return; diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index e1c482f7..817af0d2 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -5,7 +5,7 @@ using Penumbra.Mods; namespace Penumbra.Services; -public class FilenameService(DalamudPluginInterface pi) : IService +public class FilenameService(IDalamudPluginInterface pi) : IService { public readonly string ConfigDirectory = pi.ConfigDirectory.FullName; public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections"); diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 0a85a569..08118483 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -9,8 +9,8 @@ using OtterGui.Services; namespace Penumbra.Services; -public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat, INotificationManager notificationManager) - : OtterGui.Classes.MessageService(log, uiBuilder, chat, notificationManager), IService +public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) + : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { public void LinkItem(Item item) { diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 3279da96..c0dc9314 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -19,7 +19,7 @@ namespace Penumbra.Services; public static class StaticServiceManager { - public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log) + public static ServiceManager CreateProvider(Penumbra penumbra, IDalamudPluginInterface pi, Logger log) { var services = new ServiceManager(log) .AddDalamudServices(pi) @@ -40,7 +40,7 @@ public static class StaticServiceManager return services; } - private static ServiceManager AddDalamudServices(this ServiceManager services, DalamudPluginInterface pi) + private static ServiceManager AddDalamudServices(this ServiceManager services, IDalamudPluginInterface pi) => services.AddExistingService(pi) .AddExistingService(pi.UiBuilder) .AddDalamudService(pi) diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index cc70306b..cefee139 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Client.System.Framework; using OtterGui.Classes; @@ -27,11 +27,11 @@ public class ValidityChecker : IService get { var framework = Framework.Instance(); - return framework == null ? string.Empty : framework->GameVersion[0]; + return framework == null ? string.Empty : framework->GameVersionString; } } - public ValidityChecker(DalamudPluginInterface pi) + public ValidityChecker(IDalamudPluginInterface pi) { DevPenumbraExists = CheckDevPluginPenumbra(pi); IsNotInstalledPenumbra = CheckIsNotInstalled(pi); @@ -50,7 +50,7 @@ public class ValidityChecker : IService } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. - private static bool CheckDevPluginPenumbra(DalamudPluginInterface pi) + private static bool CheckDevPluginPenumbra(IDalamudPluginInterface pi) { #if !DEBUG var path = Path.Combine(pi.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra"); @@ -71,7 +71,7 @@ public class ValidityChecker : IService } // Check if the loaded version of Penumbra itself is in devPlugins. - private static bool CheckIsNotInstalled(DalamudPluginInterface pi) + private static bool CheckIsNotInstalled(IDalamudPluginInterface pi) { #if !DEBUG var checkedDirectory = pi.AssemblyLocation.Directory?.Parent?.Parent?.Name; @@ -86,7 +86,7 @@ public class ValidityChecker : IService } // Check if the loaded version of Penumbra is installed from a valid source repo. - private static bool CheckSourceRepo(DalamudPluginInterface pi) + private static bool CheckSourceRepo(IDalamudPluginInterface pi) { #if !DEBUG return pi.SourceRepository?.Trim().ToLowerInvariant() switch diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c95884c6..eeb94c71 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 652f928d..6db4db5c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 56e9482b..91129129 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Newtonsoft.Json.Linq; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 3ce10224..b9525b29 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -287,7 +287,7 @@ public partial class ModEditWindow { fixed (ushort* v2 = &v) { - return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags); + return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, nint.Zero, nint.Zero, "%04X", flags); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 070895b5..a22c10ad 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,5 +1,5 @@ -using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Lumina.Misc; using OtterGui.Raii; diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 0afeeeeb..72bfa266 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -121,7 +122,7 @@ public class ChangedItemDrawer : IDisposable, IUiService public static Vector2 TypeFilterIconSize => new(2 * ImGui.GetTextLineHeight()); - public ChangedItemDrawer(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, + public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { _items = gameData.GetExcelSheet()!; @@ -417,7 +418,7 @@ public class ChangedItemDrawer : IDisposable, IUiService }; /// Initialize the icons. - private bool CreateEquipSlotIcons(UiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) + private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) { using var equipTypeIcons = uiBuilder.LoadUld("ui/uld/ArmouryBoard.uld"); @@ -441,20 +442,20 @@ public class ChangedItemDrawer : IDisposable, IUiService Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIcon.Monster, textureProvider.GetTextureFromGame("ui/icon/062000/062042_hr1.tex", true)); - Add(ChangedItemIcon.Demihuman, textureProvider.GetTextureFromGame("ui/icon/062000/062041_hr1.tex", true)); - Add(ChangedItemIcon.Customization, textureProvider.GetTextureFromGame("ui/icon/062000/062043_hr1.tex", true)); - Add(ChangedItemIcon.Action, textureProvider.GetTextureFromGame("ui/icon/062000/062001_hr1.tex", true)); - Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, uiBuilder)); - Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, uiBuilder)); - Add(AllFlags, textureProvider.GetTextureFromGame("ui/icon/114000/114052_hr1.tex", true)); + Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062042_hr1.tex")!)); + Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062041_hr1.tex")!)); + Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); + Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); + Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider)); + Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider)); + Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); _smallestIconWidth = _icons.Values.Min(i => i.Width); return true; } - private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, UiBuilder uiBuilder) + private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) @@ -466,10 +467,10 @@ public class ChangedItemDrawer : IDisposable, IUiService for (var y = 0; y < unk.Header.Height; ++y) image.AsSpan(4 * y * unk.Header.Width, 4 * unk.Header.Width).CopyTo(bytes.AsSpan(4 * y * unk.Header.Height + diff)); - return uiBuilder.LoadImageRaw(bytes, unk.Header.Height, unk.Header.Height, 4); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(unk.Header.Height, unk.Header.Height), bytes, "Penumbra.UnkItemIcon"); } - private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, UiBuilder uiBuilder) + private static unsafe IDalamudTextureWrap? LoadEmoteTexture(IDataManager gameData, ITextureProvider textureProvider) { var emote = gameData.GetFile("ui/icon/000000/000019_hr1.tex"); if (emote == null) @@ -486,6 +487,6 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - return uiBuilder.LoadImageRaw(image2, emote.Header.Width, emote.Header.Height, 4); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon"); } } diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 082b78b8..914f10d9 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -2,7 +2,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; @@ -21,7 +21,7 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; public sealed class CollectionPanel( - DalamudPluginInterface pi, + IDalamudPluginInterface pi, CommunicatorService communicator, CollectionManager manager, CollectionSelector selector, @@ -318,7 +318,7 @@ public sealed class CollectionPanel( var button = ImGui.Button(text, width) || ImGui.IsItemClicked(ImGuiMouseButton.Right); var hovered = redundancy.Length > 0 && ImGui.IsItemHovered(); DrawIndividualDragSource(text, id); - DrawIndividualDragTarget(text, id); + DrawIndividualDragTarget(id); if (!invalid) { selector.DragTargetAssignment(type, id); @@ -349,7 +349,7 @@ public sealed class CollectionPanel( _draggedIndividualAssignment = _active.Individuals.Index(id); } - private void DrawIndividualDragTarget(string text, ActorIdentifier id) + private void DrawIndividualDragTarget(ActorIdentifier id) { if (!id.IsValid) return; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 67b0a50c..53fa0b33 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI; public sealed class ConfigWindow : Window, IUiService { - private readonly DalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private readonly Configuration _config; private readonly PerformanceTracker _tracker; private readonly ValidityChecker _validityChecker; @@ -24,7 +24,7 @@ public sealed class ConfigWindow : Window, IUiService private ConfigTabBar _configTabs = null!; private string? _lastException; - public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, + public ConfigWindow(PerformanceTracker tracker, IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) : base(GetLabel(checker)) { diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 14e16432..cb533a00 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin; using Dalamud.Plugin.Services; using OtterGui.Services; @@ -13,23 +13,25 @@ namespace Penumbra.UI; public class LaunchButton : IDisposable, IUiService { private readonly ConfigWindow _configWindow; - private readonly UiBuilder _uiBuilder; + private readonly IUiBuilder _uiBuilder; private readonly ITitleScreenMenu _title; private readonly string _fileName; + private readonly ITextureProvider _textureProvider; - private IDalamudTextureWrap? _icon; - private TitleScreenMenuEntry? _entry; + private IDalamudTextureWrap? _icon; + private IReadOnlyTitleScreenMenuEntry? _entry; /// /// Register the launch button to be created on the next draw event. /// - public LaunchButton(DalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui) + public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui, ITextureProvider textureProvider) { - _uiBuilder = pi.UiBuilder; - _configWindow = ui; - _title = title; - _icon = null; - _entry = null; + _uiBuilder = pi.UiBuilder; + _configWindow = ui; + _textureProvider = textureProvider; + _title = title; + _icon = null; + _entry = null; _fileName = Path.Combine(pi.AssemblyLocation.DirectoryName!, "tsmLogo.png"); _uiBuilder.Draw += CreateEntry; @@ -49,7 +51,8 @@ public class LaunchButton : IDisposable, IUiService { try { - _icon = _uiBuilder.LoadImage(_fileName); + // TODO: update when API updated. + _icon = _textureProvider.GetFromFile(_fileName).RentAsync().Result; if (_icon != null) _entry = _title.AddEntry("Manage Penumbra", _icon, OnTriggered); diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index b8faadf7..ec5bb920 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Classes; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0ca4d40c..88d6afa2 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.DragDrop; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 28f00a97..ee6fab1f 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -17,7 +17,7 @@ public class ModPanel : IDisposable, IUiService private readonly ModPanelTabBar _tabs; private bool _resetCursor; - public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, + public ModPanel(IDalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, MultiModPanel multiModPanel, CommunicatorService communicator) { _selector = selector; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1e371065..f81b2831 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.Components; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Raii; diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index a8b393b1..6c974f9c 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -20,7 +20,7 @@ public class ModPanelHeader : IDisposable private readonly CommunicatorService _communicator; private float _lastPreSettingsHeight = 0; - public ModPanelHeader(DalamudPluginInterface pi, CommunicatorService communicator) + public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) { _communicator = communicator; _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index d531b1a2..8de613d4 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 3bf4cd88..c53f1b8e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -271,7 +271,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService public unsafe string Name(ResolveData resolve, string none = "") { - if (resolve.AssociatedGameObject == IntPtr.Zero || !_actors.Awaiter.IsCompletedSuccessfully) + if (resolve.AssociatedGameObject == nint.Zero || !_actors.Awaiter.IsCompletedSuccessfully) return none; try diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 34e2cbcf..05a1f33b 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -38,7 +38,7 @@ public sealed class CollectionsTab : IDisposable, ITab, IUiService } } - public CollectionsTab(DalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, + public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService) { _config = configuration.Ephemeral; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 0122a6f5..41f28ab9 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -37,7 +37,6 @@ using Penumbra.Util; using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; @@ -437,8 +436,8 @@ public class DebugTab : Window, ITab, IUiService : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind == (byte)ObjectKind.BattleNpc - ? $"{identifier.DataId} | {obj.AsObject->DataID}" + var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->BaseId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); } @@ -587,11 +586,11 @@ public class DebugTab : Window, ITab, IUiService if (table) { ImGuiUtil.DrawTableColumn("Group Members"); - ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MemberCount.ToString()); + ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MainGroup.MemberCount.ToString()); for (var i = 0; i < 8; ++i) { ImGuiUtil.DrawTableColumn($"Member #{i}"); - var member = GroupManager.Instance()->GetPartyMemberByIndex(i); + var member = GroupManager.Instance()->MainGroup.GetPartyMemberByIndex(i); ImGuiUtil.DrawTableColumn(member == null ? "NULL" : new ByteString(member->Name).ToString()); } } @@ -612,7 +611,7 @@ public class DebugTab : Window, ITab, IUiService if (table) for (var i = 0; i < 8; ++i) { - ref var c = ref agent->Data->CharacterArraySpan[i]; + ref var c = ref agent->Data->Characters[i]; ImGuiUtil.DrawTableColumn($"Character {i}"); var name = c.Name1.ToString(); ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c.WorldId})"); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 7faa3da8..50fdc1d3 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -5,7 +5,7 @@ using OtterGui.Raii; using Penumbra.UI.Classes; using Dalamud.Interface; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game.Housing; +using FFXIVClientStructs.FFXIV.Client.Game; using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 0b54c5e2..14d4ed41 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -121,7 +121,7 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa } /// Obtain a label for an extension node. - private static string GetNodeLabel(uint label, uint type, ulong count) + private static string GetNodeLabel(uint label, uint type, int count) { var (lowest, mid1, mid2, highest) = Functions.SplitBytes(type); return highest == 0 diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 17db21c9..49e77a4d 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -41,7 +41,7 @@ public class SettingsTab : ITab, IUiService private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private readonly FileCompactor _compactor; private readonly DalamudConfigService _dalamudConfig; - private readonly DalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly PredefinedTagManager _predefinedTagManager; private readonly CrashHandlerService _crashService; @@ -51,7 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly TagButtons _sharedTags = new(); - public SettingsTab(DalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, + public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs index 8fbce6d0..deba7023 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/UiHelpers.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 99819fce..72ac0d01 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -9,13 +9,13 @@ namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable, IUiService { - private readonly UiBuilder _uiBuilder; + private readonly IUiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; private readonly FileDialogService _fileDialog; public readonly ConfigWindow Window; public readonly PenumbraChangelog Changelog; - public PenumbraWindowSystem(DalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, + public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab) { _uiBuilder = pi.UiBuilder; diff --git a/repo.json b/repo.json index 3142f8d4..6379595e 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 431933e9c16ac5e4e0e25504cda14eb1a670ea3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 2 Jul 2024 18:27:53 +0200 Subject: [PATCH 199/865] Revert repo API version. --- repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.json b/repo.json index 6379595e..3142f8d4 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 10, + "DalamudApiLevel": 9, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 9fb80907811f1fac339410ba60ce6384c40195a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 3 Jul 2024 17:29:49 +0200 Subject: [PATCH 200/865] Current state. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/GameStateApi.cs | 2 +- .../Cache/CollectionCacheManager.cs | 2 +- .../Collections/Cache/CustomResourceCache.cs | 2 +- Penumbra/Communication/MtrlShpkLoaded.cs | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 18 ++- .../Hooks/Objects/CharacterBaseDestructor.cs | 2 +- .../Hooks/Objects/CharacterDestructor.cs | 2 +- .../Interop/Hooks/Objects/CopyCharacter.cs | 2 +- .../Hooks/Objects/CreateCharacterBase.cs | 2 +- Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/WeaponReload.cs | 2 +- .../PreBoneDeformerReplacer.cs | 24 ++-- .../PostProcessing}/ShaderReplacementFixer.cs | 20 ++-- .../ResourceLoading/CreateFileWHook.cs | 5 +- .../ResourceLoading/FileReadService.cs | 11 +- .../ResourceLoading/ResourceLoader.cs | 26 ++-- .../ResourceLoading/ResourceManagerService.cs | 6 +- .../ResourceLoading/ResourceService.cs | 13 +- .../ResourceLoading/TexMdlService.cs | 113 +++++++++++------- .../Hooks/Resources/ApricotResourceLoad.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlShpk.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlTex.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 3 +- .../Resources/ResourceHandleDestructor.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- .../Interop/PathResolving/PathResolver.cs | 2 +- .../Interop/PathResolving/SubfileHelper.cs | 2 +- .../Processing/FilePostProcessService.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 +++-- Penumbra/Interop/Services/DecalReverter.cs | 2 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Penumbra.csproj | 1 - Penumbra/Services/CrashHandlerService.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- 39 files changed, 186 insertions(+), 139 deletions(-) rename Penumbra/Interop/{Services => Hooks/PostProcessing}/PreBoneDeformerReplacer.cs (81%) rename Penumbra/Interop/{Services => Hooks/PostProcessing}/ShaderReplacementFixer.cs (91%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/CreateFileWHook.cs (97%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/FileReadService.cs (92%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/ResourceLoader.cs (93%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/ResourceManagerService.cs (93%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/ResourceService.cs (95%) rename Penumbra/Interop/{ => Hooks}/ResourceLoading/TexMdlService.cs (60%) diff --git a/OtterGui b/OtterGui index 437ef65c..c2738e1d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 437ef65c6464c54c8f40196dd2428da901d73aab +Subproject commit c2738e1d42974cddbe5a31238c6ed236a831d17d diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a97e5ae..066637ab 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a97e5aeee3b7375b333c1add5305d0ce80cbf83 +Subproject commit 066637abe05c659b79d84f52e6db33487498f433 diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index becb55ee..b035c886 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -2,8 +2,8 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String.Classes; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 44c12856..80d4cf1d 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -5,7 +5,7 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; diff --git a/Penumbra/Collections/Cache/CustomResourceCache.cs b/Penumbra/Collections/Cache/CustomResourceCache.cs index 46c28393..e63f8637 100644 --- a/Penumbra/Collections/Cache/CustomResourceCache.cs +++ b/Penumbra/Collections/Cache/CustomResourceCache.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 8aab0e0e..9d3597a8 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper + /// ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 6d511a28..ed4eb669 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -1,8 +1,14 @@ -namespace Penumbra.Interop.Hooks; - +namespace Penumbra.Interop.Hooks; + public static class HookSettings { - public const bool MetaEntryHooks = false; - public const bool MetaParentHooks = false; - public const bool VfxIdentificationHooks = false; -} + public const bool AllHooks = true; + + public const bool ObjectHooks = false && AllHooks; + public const bool ReplacementHooks = true && AllHooks; + public const bool ResourceHooks = false && AllHooks; + public const bool MetaEntryHooks = false && AllHooks; + public const bool MetaParentHooks = false && AllHooks; + public const bool VfxIdentificationHooks = false && AllHooks; + public const bool PostProcessingHooks = false && AllHooks; +} diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index e01a6550..c67bb9f3 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index 6e10c5e3..618d0bd7 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, true); + => _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 20c96f56..663209ae 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 299f312a..56b3d853 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -16,7 +16,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 267b4711..8b701fe5 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -17,7 +17,7 @@ public sealed unsafe class EnableDraw : IHookService public EnableDraw(HookManager hooks, GameState state) { _state = state; - _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, true); + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, HookSettings.ObjectHooks); } private delegate void Delegate(GameObject* gameObject); diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index fec0a13f..da31840f 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -16,7 +16,7 @@ public sealed unsafe class WeaponReload : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, true); + => _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs similarity index 81% rename from Penumbra/Interop/Services/PreBoneDeformerReplacer.cs rename to Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 9f553257..903484ea 100644 --- a/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -4,12 +4,13 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.SafeHandles; using Penumbra.String.Classes; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -namespace Penumbra.Interop.Services; +namespace Penumbra.Interop.Hooks.PostProcessing; public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService { @@ -29,17 +30,16 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi private readonly IFramework _framework; public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, - IGameInteropProvider interop, IFramework framework, CharacterBaseVTables vTables) + HookManager hooks, IFramework framework, CharacterBaseVTables vTables) { - interop.InitializeFromAttributes(this); - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = interop.HookFromAddress(vTables.HumanVTable[57], SetupScaling); - _humanCreateDeformerHook = interop.HookFromAddress(vTables.HumanVTable[91], CreateDeformer); - _humanSetupScalingHook.Enable(); - _humanCreateDeformerHook.Enable(); + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, + HookSettings.PostProcessingHooks).Result; + _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], + CreateDeformer, HookSettings.PostProcessingHooks).Result; } public void Dispose() diff --git a/Penumbra/Interop/Services/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs similarity index 91% rename from Penumbra/Interop/Services/ShaderReplacementFixer.cs rename to Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 95e70b45..b27ca4c5 100644 --- a/Penumbra/Interop/Services/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -1,6 +1,4 @@ using Dalamud.Hooking; -using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -10,9 +8,11 @@ using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; using Penumbra.Services; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; +using ModelRenderer = Penumbra.Interop.Services.ModelRenderer; -namespace Penumbra.Interop.Services; +namespace Penumbra.Interop.Hooks.PostProcessing; public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredService { @@ -29,8 +29,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly Hook _humanOnRenderMaterialHook; - [Signature(Sigs.ModelRendererOnRenderMaterial, DetourName = nameof(ModelRendererOnRenderMaterialDetour))] - private readonly Hook _modelRendererOnRenderMaterialHook = null!; + private readonly Hook _modelRendererOnRenderMaterialHook; private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; @@ -59,19 +58,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _moddedCharacterGlassShpkCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, IGameInteropProvider interop, CharacterBaseVTables vTables) + CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) { - interop.InitializeFromAttributes(this); _resourceHandleDestructor = resourceHandleDestructor; _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; - _humanOnRenderMaterialHook = - interop.HookFromAddress(vTables.HumanVTable[62], OnRenderHumanMaterial); + _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[62], + OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; + _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", + Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); - _humanOnRenderMaterialHook.Enable(); - _modelRendererOnRenderMaterialHook.Enable(); } public void Dispose() diff --git a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs similarity index 97% rename from Penumbra/Interop/ResourceLoading/CreateFileWHook.cs rename to Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs index bde640d2..a8ac0608 100644 --- a/Penumbra/Interop/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs @@ -5,7 +5,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.String.Functions; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; /// /// To allow XIV to load files of arbitrary path length, @@ -19,7 +19,8 @@ public unsafe class CreateFileWHook : IDisposable, IRequiredService public CreateFileWHook(IGameInteropProvider interop) { _createFileWHook = interop.HookFromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); - _createFileWHook.Enable(); + if (HookSettings.ReplacementHooks) + _createFileWHook.Enable(); } /// diff --git a/Penumbra/Interop/ResourceLoading/FileReadService.cs b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs similarity index 92% rename from Penumbra/Interop/ResourceLoading/FileReadService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs index 5edba790..199525fb 100644 --- a/Penumbra/Interop/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs @@ -6,16 +6,17 @@ using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.Util; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class FileReadService : IDisposable, IRequiredService { public FileReadService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _resourceManager = resourceManager; - _performance = performance; + _performance = performance; interop.InitializeFromAttributes(this); - _readSqPackHook.Enable(); + if (HookSettings.ReplacementHooks) + _readSqPackHook.Enable(); } /// Invoked when a file is supposed to be read from SqPack. @@ -49,7 +50,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService _readSqPackHook.Dispose(); } - private readonly PerformanceTracker _performance; + private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; private delegate byte ReadSqPackPrototype(nint resourceManager, SeFileDescriptor* pFileDesc, int priority, bool isSync); @@ -60,7 +61,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService private byte ReadSqPackDetour(nint resourceManager, SeFileDescriptor* fileDescriptor, int priority, bool isSync) { using var performance = _performance.Measure(PerformanceType.ReadSqPack); - byte? ret = null; + byte? ret = null; _lastFileThreadResourceManager.Value = resourceManager; ReadSqPack?.Invoke(fileDescriptor, ref priority, ref isSync, ref ret); _lastFileThreadResourceManager.Value = nint.Zero; diff --git a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs similarity index 93% rename from Penumbra/Interop/ResourceLoading/ResourceLoader.cs rename to Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 4a423993..5cac2f32 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -9,27 +9,27 @@ using Penumbra.String; using Penumbra.String.Classes; using FileMode = Penumbra.Interop.Structs.FileMode; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; - private readonly TexMdlService _texMdlService; + private readonly TexMdlService _texMdlService; private ResolveData _resolvedData = ResolveData.Invalid; public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { - _resources = resources; + _resources = resources; _fileReadService = fileReadService; - _texMdlService = texMdlService; + _texMdlService = texMdlService; ResetResolvePath(); - _resources.ResourceRequested += ResourceHandler; + _resources.ResourceRequested += ResourceHandler; _resources.ResourceHandleIncRef += IncRefProtection; _resources.ResourceHandleDecRef += DecRefProtection; - _fileReadService.ReadSqPack += ReadSqPackDetour; + _fileReadService.ReadSqPack += ReadSqPackDetour; } /// Load a resource for a given path and a specific collection. @@ -80,10 +80,10 @@ public unsafe class ResourceLoader : IDisposable, IService public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceRequested -= ResourceHandler; _resources.ResourceHandleIncRef -= IncRefProtection; _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _fileReadService.ReadSqPack -= ReadSqPackDetour; } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, @@ -112,7 +112,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; - path = p; + path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } @@ -140,12 +140,12 @@ public unsafe class ResourceLoader : IDisposable, IService } var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); - fileDescriptor->ResourceHandle->FileNameData = path.Path; + fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; } @@ -165,7 +165,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. var fd = stackalloc char[0x11 + 0x0B + 14]; fileDescriptor->FileDescriptor = (byte*)fd + 1; - CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); // Use the SE ReadFile function. @@ -206,7 +206,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; _incMode.Value = true; - returnValue = _resources.IncRef(handle); + returnValue = _resources.IncRef(handle); _incMode.Value = false; } diff --git a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceManagerService.cs similarity index 93% rename from Penumbra/Interop/ResourceLoading/ResourceManagerService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/ResourceManagerService.cs index 0479d2a6..1bff80ba 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceManagerService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceManagerService.cs @@ -8,7 +8,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceManagerService : IRequiredService { @@ -23,10 +23,10 @@ public unsafe class ResourceManagerService : IRequiredService public ResourceHandle* FindResource(ResourceCategory cat, ResourceType ext, uint crc32) { ref var manager = ref *ResourceManager; - var catIdx = (uint)cat >> 0x18; + var catIdx = (uint)cat >> 0x18; cat = (ResourceCategory)(ushort)cat; ref var category = ref manager.ResourceGraph->Containers[(int)cat]; - var extMap = FindInMap(category.CategoryMaps[(int)catIdx].Value, (uint)ext); + var extMap = FindInMap(category.CategoryMaps[(int)catIdx].Value, (uint)ext); if (extMap == null) return null; diff --git a/Penumbra/Interop/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs similarity index 95% rename from Penumbra/Interop/ResourceLoading/ResourceService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index d623d72d..0b00452b 100644 --- a/Penumbra/Interop/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -12,7 +12,7 @@ using Penumbra.String.Classes; using Penumbra.Util; using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceService : IDisposable, IRequiredService { @@ -24,16 +24,19 @@ public unsafe class ResourceService : IDisposable, IRequiredService _performance = performance; _resourceManager = resourceManager; interop.InitializeFromAttributes(this); - _getResourceSyncHook.Enable(); - _getResourceAsyncHook.Enable(); _incRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.IncRef, ResourceHandleIncRefDetour); - _incRefHook.Enable(); _decRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); - _decRefHook.Enable(); + if (HookSettings.ReplacementHooks) + { + _getResourceSyncHook.Enable(); + _getResourceAsyncHook.Enable(); + _incRefHook.Enable(); + _decRefHook.Enable(); + } } public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path) diff --git a/Penumbra/Interop/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs similarity index 60% rename from Penumbra/Interop/ResourceLoading/TexMdlService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index a2b43c64..28ad7aa4 100644 --- a/Penumbra/Interop/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -1,109 +1,138 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.LayoutEngine; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using Lumina.Excel.GeneratedSheets2; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; +using Penumbra.Interop.Structs; using Penumbra.String.Classes; +using FileMode = Penumbra.Interop.Structs.FileMode; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; -namespace Penumbra.Interop.ResourceLoading; +namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class TexMdlService : IDisposable, IRequiredService { /// Custom ulong flag to signal our files as opposed to SE files. public static readonly nint CustomFileFlag = new(0xDEADBEEF); - + /// /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// public IReadOnlySet CustomFileCrc => _customFileCrc; - + public TexMdlService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); - //_checkFileStateHook.Enable(); - //_loadTexFileExternHook.Enable(); - //_loadMdlFileExternHook.Enable(); + if (HookSettings.ReplacementHooks) + { + _checkFileStateHook.Enable(); + _loadMdlFileExternHook.Enable(); + _textureSomethingHook.Enable(); + _vf32Hook.Enable(); + //_loadTexFileExternHook.Enable(); + } } - + /// Add CRC64 if the given file is a model or texture file and has an associated path. public void AddCrc(ResourceType type, FullPath? path) { - if (path.HasValue && type is ResourceType.Mdl or ResourceType.Tex) + if (path.HasValue && type is ResourceType.Mdl) _customFileCrc.Add(path.Value.Crc64); } - + /// Add a fixed CRC64 value. public void AddCrc(ulong crc64) => _customFileCrc.Add(crc64); - + public void Dispose() { - //_checkFileStateHook.Dispose(); + _checkFileStateHook.Dispose(); //_loadTexFileExternHook.Dispose(); - //_loadMdlFileExternHook.Dispose(); + _textureSomethingHook.Dispose(); + _loadMdlFileExternHook.Dispose(); + _vf32Hook.Dispose(); } - - private readonly HashSet _customFileCrc = new(); - + + private readonly HashSet _customFileCrc = []; + private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); - + + private delegate nint TextureSomethingDelegate(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor); + [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] private readonly Hook _checkFileStateHook = null!; - + + [Signature("E8 ?? ?? ?? ?? 0F B6 C8 EB ?? 4C 8B 83", DetourName = nameof(TextureSomethingDetour))] + private readonly Hook _textureSomethingHook = null!; + + private nint TextureSomethingDetour(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor) + { + //Penumbra.Log.Information($"SomethingDetour {handle->Handle.FileName()}"); + //if (!handle->Handle.GamePath(out var path) || !path.IsRooted()) + return _textureSomethingHook.Original(handle, lod, descriptor); + + descriptor->FileMode = FileMode.LoadUnpackedResource; + return _loadTexFileLocal.Invoke((ResourceHandle*)handle, lod, (nint)descriptor, true); + } + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. /// private nint CheckFileStateDetour(nint ptr, ulong crc64) => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); - - + + private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, nint unk2, bool unk3); - + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadTexFileLocal)] private readonly LoadTexFileLocalDelegate _loadTexFileLocal = null!; - + private delegate byte LoadMdlFileLocalPrototype(ResourceHandle* handle, nint unk1, bool unk2); - + /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadMdlFileLocal)] private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; - - + + private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, nint unk2, bool unk3, nint unk4); + + private delegate byte TexResourceHandleVf32Prototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); + + [Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] + private readonly Hook _vf32Hook = null!; - private delegate byte TexResourceHandleVf32Prototype(ResourceHandle* handle, nint unk1, byte unk2); - - //[Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] - //private readonly Hook _vf32Hook = null!; - // - //private byte Vf32Detour(ResourceHandle* handle, nint unk1, byte unk2) - //{ - // var ret = _vf32Hook.Original(handle, unk1, unk2); - // return _loadTexFileLocal() - //} - + private byte Vf32Detour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) + { + //if (handle->Handle.GamePath(out var path) && path.IsRooted()) + //{ + // Penumbra.Log.Information($"Replacing {descriptor->FileMode} with {FileMode.LoadSqPackResource} in VF32 for {path}."); + // descriptor->FileMode = FileMode.LoadSqPackResource; + //} + + var ret = _vf32Hook.Original(handle, descriptor, unk2); + return ret; + } + //[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] //private readonly Hook _loadTexFileExternHook = null!; - + /// We hook the extern functions to just return the local one if given the custom flag as last argument. //private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, nint unk2, bool unk3, nint ptr) // => ptr.Equals(CustomFileFlag) // ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) // : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); - public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); - - + + [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] private readonly Hook _loadMdlFileExternHook = null!; - + /// We hook the extern functions to just return the local one if given the custom flag as last argument. private byte LoadMdlFileExternDetour(ResourceHandle* resourceHandle, nint unk1, bool unk2, nint ptr) => ptr.Equals(CustomFileFlag) diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs index 2e5698a3..511e842f 100644 --- a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -11,7 +11,7 @@ public sealed unsafe class ApricotResourceLoad : FastHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, true); + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookSettings.ResourceHooks); } public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs index 5ef3bf37..8447762b 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -14,7 +14,7 @@ public sealed unsafe class LoadMtrlShpk : FastHook { _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, true); + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookSettings.ResourceHooks); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index 14a011ea..7bc3c7b0 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -11,7 +11,7 @@ public sealed unsafe class LoadMtrlTex : FastHook public LoadMtrlTex(HookManager hooks, GameState gameState) { _gameState = gameState; - Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, true); + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookSettings.ResourceHooks); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index a7e82b72..8fa6d861 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -59,7 +59,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveVfxPathHook = Create( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman); _resolveEidPathHook = Create( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid); // @formatter:on - Enable(); + if (HookSettings.ResourceHooks) + Enable(); } public void Enable() diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index ac3f504a..cd4a53c4 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -23,7 +23,7 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, true); + => _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, HookSettings.ResourceHooks); private readonly Task> _task; diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index 5eacbfb0..e709c210 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -4,12 +4,12 @@ using OtterGui.Services; using Penumbra.Collections; using Penumbra.Api.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Services; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index cc3e0e9b..49035dc8 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -3,8 +3,8 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Processing; -using Penumbra.Interop.ResourceLoading; using Penumbra.String.Classes; using Penumbra.Util; diff --git a/Penumbra/Interop/PathResolving/SubfileHelper.cs b/Penumbra/Interop/PathResolving/SubfileHelper.cs index 44a152f0..836cf731 100644 --- a/Penumbra/Interop/PathResolving/SubfileHelper.cs +++ b/Penumbra/Interop/PathResolving/SubfileHelper.cs @@ -1,8 +1,8 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.Resources; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String.Classes; diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index 0dc62b3d..bba53c94 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -1,7 +1,7 @@ using System.Collections.Frozen; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index ca8836b0..a852a4cc 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -9,8 +9,8 @@ using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index b8bad84a..810c946d 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -5,7 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Interop.Services; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; @@ -31,7 +31,8 @@ public class ResourceTree public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) + public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, + bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) { Name = name; AnonymizedName = anonymizedName; @@ -61,9 +62,10 @@ public class ResourceTree var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; var equipment = modelType switch { - CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan(Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), - _ => ReadOnlySpan.Empty, + CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), + CharacterBase.ModelType.DemiHuman => new ReadOnlySpan( + Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), + _ => ReadOnlySpan.Empty, }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; @@ -112,15 +114,17 @@ public class ResourceTree { if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; + var subObject = (CharacterBase*)baseSubObject; if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) continue; - var weapon = (Weapon*)subObject; + + var weapon = (Weapon*)subObject; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; - var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown); + var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain1, weapon->Stain2)); var weaponType = weapon->SecondaryId; var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); @@ -152,6 +156,7 @@ public class ResourceTree ++weaponIndex; } + Nodes.InsertRange(0, weaponNodes); } @@ -167,10 +172,11 @@ public class ResourceTree { if (globalContext.WithUiData) { - pbdNode = pbdNode.Clone(); + pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; } + Nodes.Add(pbdNode); } } @@ -184,10 +190,11 @@ public class ResourceTree { if (globalContext.WithUiData) { - decalNode = decalNode.Clone(); + decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; } + Nodes.Add(decalNode); } @@ -200,10 +207,11 @@ public class ResourceTree { if (globalContext.WithUiData) { - legacyDecalNode = legacyDecalNode.Clone(); + legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; } + Nodes.Add(legacyDecalNode); } } diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 17d8d2e0..21b51fd2 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -1,7 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b1ad0b78..9f2db2e6 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -8,7 +8,6 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Cache; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Services; using Penumbra.Interop.Services; @@ -22,6 +21,7 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra; diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index ed5c5e30..2e53bd22 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -37,7 +37,6 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\ - H:\Projects\FFPlugins\Dalamud\bin\Release\ diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 25c6cf57..9103b29c 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -7,8 +7,8 @@ using Penumbra.Communication; using Penumbra.CrashHandler; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData.Actors; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; using Penumbra.String.Classes; diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index c53f1b8e..935f11e3 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -8,8 +8,8 @@ using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.Resources; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.Services; using Penumbra.String; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 41f28ab9..9a03f384 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -25,7 +25,6 @@ using Penumbra.GameData.Interop; using Penumbra.Import.Structs; using Penumbra.Import.Textures; using Penumbra.Interop.PathResolving; -using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Mods; @@ -40,6 +39,8 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra.UI.Tabs.Debug; diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 14d4ed41..a4dbba2f 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -8,7 +8,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; -using Penumbra.Interop.ResourceLoading; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; From 4026dd58672fb38a6c83dfe1309ded1b10a9aa67 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 12:14:31 +0200 Subject: [PATCH 201/865] Change texture handling. --- Penumbra.GameData | 2 +- .../Hooks/ResourceLoading/TexMdlService.cs | 128 +++++++++--------- Penumbra/Interop/Structs/ResourceHandle.cs | 6 + 3 files changed, 73 insertions(+), 63 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 066637ab..19923f8d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 066637abe05c659b79d84f52e6db33487498f433 +Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 28ad7aa4..793d15af 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -1,93 +1,110 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; -using Lumina.Excel.GeneratedSheets2; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String.Classes; -using FileMode = Penumbra.Interop.Structs.FileMode; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class TexMdlService : IDisposable, IRequiredService { + /// + /// We need to be able to obtain the requested LoD level. + /// This replicates the LoD behavior of a textures OnLoad function. + /// + private readonly struct LodService + { + public LodService(IGameInteropProvider interop) + => interop.InitializeFromAttributes(this); + + [Signature(Sigs.LodConfig)] + private readonly nint _lodConfig = nint.Zero; + + public byte GetLod(TextureResourceHandle* handle) + { + if (handle->ChangeLod) + { + var config = *(byte*)_lodConfig + 0xE; + if (config == byte.MaxValue) + return 2; + } + + return 0; + } + } + /// Custom ulong flag to signal our files as opposed to SE files. public static readonly nint CustomFileFlag = new(0xDEADBEEF); - /// - /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, - /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. - /// - public IReadOnlySet CustomFileCrc - => _customFileCrc; + private readonly LodService _lodService; public TexMdlService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); + _lodService = new LodService(interop); if (HookSettings.ReplacementHooks) { _checkFileStateHook.Enable(); _loadMdlFileExternHook.Enable(); - _textureSomethingHook.Enable(); - _vf32Hook.Enable(); - //_loadTexFileExternHook.Enable(); + _textureOnLoadHook.Enable(); } } /// Add CRC64 if the given file is a model or texture file and has an associated path. public void AddCrc(ResourceType type, FullPath? path) { - if (path.HasValue && type is ResourceType.Mdl) - _customFileCrc.Add(path.Value.Crc64); + _ = type switch + { + ResourceType.Mdl when path.HasValue => _customMdlCrc.Add(path.Value.Crc64), + ResourceType.Tex when path.HasValue => _customTexCrc.Add(path.Value.Crc64), + _ => false, + }; } - /// Add a fixed CRC64 value. - public void AddCrc(ulong crc64) - => _customFileCrc.Add(crc64); - public void Dispose() { _checkFileStateHook.Dispose(); - //_loadTexFileExternHook.Dispose(); - _textureSomethingHook.Dispose(); _loadMdlFileExternHook.Dispose(); - _vf32Hook.Dispose(); + _textureOnLoadHook.Dispose(); } - private readonly HashSet _customFileCrc = []; + /// + /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, + /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. + /// + private readonly HashSet _customMdlCrc = []; + + private readonly HashSet _customTexCrc = []; private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); - private delegate nint TextureSomethingDelegate(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor); - [Signature(Sigs.CheckFileState, DetourName = nameof(CheckFileStateDetour))] private readonly Hook _checkFileStateHook = null!; - [Signature("E8 ?? ?? ?? ?? 0F B6 C8 EB ?? 4C 8B 83", DetourName = nameof(TextureSomethingDetour))] - private readonly Hook _textureSomethingHook = null!; - - private nint TextureSomethingDetour(TextureResourceHandle* handle, int lod, SeFileDescriptor* descriptor) - { - //Penumbra.Log.Information($"SomethingDetour {handle->Handle.FileName()}"); - //if (!handle->Handle.GamePath(out var path) || !path.IsRooted()) - return _textureSomethingHook.Original(handle, lod, descriptor); - - descriptor->FileMode = FileMode.LoadUnpackedResource; - return _loadTexFileLocal.Invoke((ResourceHandle*)handle, lod, (nint)descriptor, true); - } + private readonly ThreadLocal _texReturnData = new(() => default); /// /// The function that checks a files CRC64 to determine whether it is 'protected'. - /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag. + /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models. + /// Since Dawntrail inlined the RSF function for textures, we can not use the flag method here. + /// Instead, we signal the caller that this will fail and let it call the local function after intentionally failing. /// private nint CheckFileStateDetour(nint ptr, ulong crc64) - => _customFileCrc.Contains(crc64) ? CustomFileFlag : _checkFileStateHook.Original(ptr, crc64); + { + if (_customMdlCrc.Contains(crc64)) + return CustomFileFlag; + if (_customTexCrc.Contains(crc64)) + _texReturnData.Value = true; - private delegate byte LoadTexFileLocalDelegate(ResourceHandle* handle, int unk1, nint unk2, bool unk3); + return _checkFileStateHook.Original(ptr, crc64); + } + + private delegate byte LoadTexFileLocalDelegate(TextureResourceHandle* handle, int unk1, SeFileDescriptor* unk2, bool unk3); /// We use the local functions for our own files in the extern hook. [Signature(Sigs.LoadTexFileLocal)] @@ -99,36 +116,23 @@ public unsafe class TexMdlService : IDisposable, IRequiredService [Signature(Sigs.LoadMdlFileLocal)] private readonly LoadMdlFileLocalPrototype _loadMdlFileLocal = null!; + private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - private delegate byte LoadTexFileExternPrototype(ResourceHandle* handle, int unk1, nint unk2, bool unk3, nint unk4); + [Signature(Sigs.TexResourceHandleOnLoad, DetourName = nameof(OnLoadDetour))] + private readonly Hook _textureOnLoadHook = null!; - private delegate byte TexResourceHandleVf32Prototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - - [Signature("40 53 55 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B D9", DetourName = nameof(Vf32Detour))] - private readonly Hook _vf32Hook = null!; - - private byte Vf32Detour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) + private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) { - //if (handle->Handle.GamePath(out var path) && path.IsRooted()) - //{ - // Penumbra.Log.Information($"Replacing {descriptor->FileMode} with {FileMode.LoadSqPackResource} in VF32 for {path}."); - // descriptor->FileMode = FileMode.LoadSqPackResource; - //} + var ret = _textureOnLoadHook.Original(handle, descriptor, unk2); + if (!_texReturnData.Value) + return ret; - var ret = _vf32Hook.Original(handle, descriptor, unk2); - return ret; + // Function failed on a replaced texture, call local. + _texReturnData.Value = false; + return _loadTexFileLocal(handle, _lodService.GetLod(handle), descriptor, unk2 != 0); } - //[Signature(Sigs.LoadTexFileExtern, DetourName = nameof(LoadTexFileExternDetour))] - //private readonly Hook _loadTexFileExternHook = null!; - - /// We hook the extern functions to just return the local one if given the custom flag as last argument. - //private byte LoadTexFileExternDetour(ResourceHandle* resourceHandle, int unk1, nint unk2, bool unk3, nint ptr) - // => ptr.Equals(CustomFileFlag) - // ? _loadTexFileLocal.Invoke(resourceHandle, unk1, unk2, unk3) - // : _loadTexFileExternHook.Original(resourceHandle, unk1, unk2, unk3, ptr); - public delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); - + private delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); [Signature(Sigs.LoadMdlFileExtern, DetourName = nameof(LoadMdlFileExternDetour))] private readonly Hook _loadMdlFileExternHook = null!; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 058b9004..6e428f25 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -14,6 +14,12 @@ public unsafe struct TextureResourceHandle [FieldOffset(0x0)] public CsHandle.TextureResourceHandle CsHandle; + + [FieldOffset(0x104)] + public byte SomeLodFlag; + + public bool ChangeLod + => (SomeLodFlag & 1) != 0; } public enum LoadState : byte From 1284037554fdeebd4e21d7a3ed846629b1c43e6b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 14:39:03 +0200 Subject: [PATCH 202/865] Fix some hooks. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 10 ++-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 1 - Penumbra/Interop/Hooks/Meta/GmpHook.cs | 47 ++++--------------- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 ++-- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 ++-- .../Interop/Hooks/Objects/CopyCharacter.cs | 5 +- .../Hooks/Objects/CreateCharacterBase.cs | 6 +-- .../Interop/Structs/CharacterUtilityData.cs | 10 ++-- Penumbra/Interop/Structs/MetaIndex.cs | 13 +++-- 11 files changed, 40 insertions(+), 75 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19923f8d..27d15145 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 +Subproject commit 27d15145567c11e2bb1902857f8db25f02189390 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index ed4eb669..9dd8d74f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -4,11 +4,11 @@ public static class HookSettings { public const bool AllHooks = true; - public const bool ObjectHooks = false && AllHooks; + public const bool ObjectHooks = true && AllHooks; public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = false && AllHooks; - public const bool MetaEntryHooks = false && AllHooks; - public const bool MetaParentHooks = false && AllHooks; + public const bool ResourceHooks = true && AllHooks; + public const bool MetaEntryHooks = true && AllHooks; + public const bool MetaParentHooks = true && AllHooks; public const bool VfxIdentificationHooks = false && AllHooks; - public const bool PostProcessingHooks = false && AllHooks; + public const bool PostProcessingHooks = true && AllHooks; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 3767c4a2..e90674a8 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 329a8beb..12b221d9 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Data.Parsing.Uld; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -8,12 +10,10 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class GmpHook : FastHook, IDisposable { - public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + public delegate ulong Delegate(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId); private readonly MetaState _metaState; - private static readonly Finalizer StablePointer = new(); - public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; @@ -21,50 +21,21 @@ public unsafe class GmpHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - /// - /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . - /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, - /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. - /// - private nint Detour(nint gmpResource, uint dividedHeadId) + private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) { - nint ret; + ulong ret; if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) - { - if (entry.Entry.Enabled) - { - *StablePointer.Pointer = entry.Entry.Value; - // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. - // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); - } - else - { - ret = nint.Zero; - } - } + ret = (*outputEntry) = entry.Entry.Enabled ? entry.Entry.Value : 0ul; else - { - ret = Task.Result.Original(gmpResource, dividedHeadId); - } + ret = Task.Result.Original(characterUtility, outputEntry, setId); - Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + Penumbra.Log.Excessive( + $"[GetGmpFlags] Invoked on 0x{(ulong)characterUtility:X} for {setId} with 0x{(ulong)outputEntry:X} (={*outputEntry:X}), returned {ret:X10}."); return ret; } - /// Allocate and clean up our single stable ulong pointer. - private class Finalizer - { - public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); - - ~Finalizer() - { - Marshal.FreeHGlobal((nint)Pointer); - } - } - public void Dispose() => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 79e7f6a6..c1803745 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index eb8a8a37..e08dc393 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -10,8 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class RspBustHook : FastHook, IDisposable { - public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, - byte bustSize); + public delegate float* Delegate(nint cmpResource, float* storage, SubRace race, byte gender, byte bodyType, byte bustSize); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -24,7 +23,7 @@ public unsafe class RspBustHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) { if (gender == 0) { @@ -38,7 +37,6 @@ public unsafe class RspBustHook : FastHook, IDisposable if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); @@ -58,11 +56,11 @@ public unsafe class RspBustHook : FastHook, IDisposable } else { - ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + ret = Task.Result.Original(cmpResource, storage, clan, gender, bodyType, bustSize); } Penumbra.Log.Excessive( - $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index f8f9e51e..20e3c939 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public class RspHeightHook : FastHook, IDisposable { - public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + public delegate float Delegate(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -23,7 +23,7 @@ public class RspHeightHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) { float scale; if (bodyType < 2 @@ -36,7 +36,6 @@ public class RspHeightHook : FastHook, IDisposable if (height > 100) height = 0; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); @@ -68,11 +67,11 @@ public class RspHeightHook : FastHook, IDisposable } else { - scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + scale = Task.Result.Original(cmpResource, clan, gender, bodyType, height); } Penumbra.Log.Excessive( - $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {height}, returned {scale}."); return scale; } diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 663209ae..d81043c8 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -9,7 +9,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr + /// CutsceneService = 0, } @@ -38,8 +38,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtrOwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); Invoke(character, source); return _task.Result.Original(target, source, unk); diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 56b3d853..f00a9984 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -10,7 +10,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// MetaState = 0, } @@ -64,10 +64,10 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// DrawObjectState = 0, - /// + /// MetaState = 0, } } diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 22150cc1..d33da477 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -6,16 +6,16 @@ namespace Penumbra.Interop.Structs; public unsafe struct CharacterUtilityData { public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 72; - public const int IndexDecalTex = 73; - public const int IndexSkinShpk = 76; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexSkinShpk = 83; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 87; + public const int TotalNumResources = 89; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) @@ -36,7 +36,7 @@ public unsafe struct CharacterUtilityData 1301 => accessory ? MetaIndex.Eqdp1301Acc : MetaIndex.Eqdp1301, 1401 => accessory ? MetaIndex.Eqdp1401Acc : MetaIndex.Eqdp1401, 1501 => accessory ? MetaIndex.Eqdp1501Acc : MetaIndex.Eqdp1501, - //1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, Female Hrothgar + 1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, 1701 => accessory ? MetaIndex.Eqdp1701Acc : MetaIndex.Eqdp1701, 1801 => accessory ? MetaIndex.Eqdp1801Acc : MetaIndex.Eqdp1801, 0104 => accessory ? MetaIndex.Eqdp0104Acc : MetaIndex.Eqdp0104, diff --git a/Penumbra/Interop/Structs/MetaIndex.cs b/Penumbra/Interop/Structs/MetaIndex.cs index 65302264..2ec5fce4 100644 --- a/Penumbra/Interop/Structs/MetaIndex.cs +++ b/Penumbra/Interop/Structs/MetaIndex.cs @@ -4,6 +4,7 @@ namespace Penumbra.Interop.Structs; public enum MetaIndex : int { Eqp = 0, + Evp = 1, Gmp = 2, Eqdp0101 = 3, @@ -21,9 +22,8 @@ public enum MetaIndex : int Eqdp1301, Eqdp1401, Eqdp1501, - - //Eqdp1601, // TODO: female Hrothgar - Eqdp1701 = Eqdp1501 + 2, + Eqdp1601, + Eqdp1701, Eqdp1801, Eqdp0104, Eqdp0204, @@ -51,9 +51,8 @@ public enum MetaIndex : int Eqdp1301Acc, Eqdp1401Acc, Eqdp1501Acc, - - //Eqdp1601Acc, // TODO: female Hrothgar - Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1601Acc, + Eqdp1701Acc, Eqdp1801Acc, Eqdp0104Acc, Eqdp0204Acc, @@ -66,7 +65,7 @@ public enum MetaIndex : int Eqdp9104Acc, Eqdp9204Acc, - HumanCmp = 64, + HumanCmp = 71, FaceEst, HairEst, HeadEst, From 41d271213efb97c34d173f3759d110780bd81d5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 5 Jul 2024 23:59:22 +0200 Subject: [PATCH 203/865] Update ShaderReplacementFixer for 7.0 --- .../PostProcessing/ShaderReplacementFixer.cs | 250 ++++++++++++++---- Penumbra/Interop/Services/ModelRenderer.cs | 82 +++++- Penumbra/UI/Tabs/Debug/DebugTab.cs | 97 +++++-- 3 files changed, 359 insertions(+), 70 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b27ca4c5..b87d33ef 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -19,9 +19,24 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; + public static ReadOnlySpan IrisShpkName + => "iris.shpk"u8; + public static ReadOnlySpan CharacterGlassShpkName => "characterglass.shpk"u8; + public static ReadOnlySpan CharacterTransparencyShpkName + => "charactertransparency.shpk"u8; + + public static ReadOnlySpan CharacterTattooShpkName + => "charactertattoo.shpk"u8; + + public static ReadOnlySpan CharacterOcclusionShpkName + => "characterocclusion.shpk"u8; + + public static ReadOnlySpan HairMaskShpkName + => "hairmask.shpk"u8; + private delegate nint CharacterBaseOnRenderMaterialDelegate(CharacterBase* drawObject, CSModelRenderer.OnRenderMaterialParams* param); private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, @@ -36,26 +51,36 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly CharacterUtility _utility; private readonly ModelRenderer _modelRenderer; - // MaterialResourceHandle set - private readonly ConcurrentSet _moddedSkinShpkMaterials = new(); - private readonly ConcurrentSet _moddedCharacterGlassShpkMaterials = new(); - - private readonly object _skinLock = new(); - private readonly object _characterGlassLock = new(); - - // ConcurrentDictionary.Count uses a lock in its current implementation. - private int _moddedSkinShpkCount; - private int _moddedCharacterGlassShpkCount; - private ulong _skinSlowPathCallDelta; - private ulong _characterGlassSlowPathCallDelta; + private readonly ModdedShaderPackageState _skinState; + private readonly ModdedShaderPackageState _irisState; + private readonly ModdedShaderPackageState _characterGlassState; + private readonly ModdedShaderPackageState _characterTransparencyState; + private readonly ModdedShaderPackageState _characterTattooState; + private readonly ModdedShaderPackageState _characterOcclusionState; + private readonly ModdedShaderPackageState _hairMaskState; public bool Enabled { get; internal set; } = true; - public int ModdedSkinShpkCount - => _moddedSkinShpkCount; + public uint ModdedSkinShpkCount + => _skinState.MaterialCount; - public int ModdedCharacterGlassShpkCount - => _moddedCharacterGlassShpkCount; + public uint ModdedIrisShpkCount + => _irisState.MaterialCount; + + public uint ModdedCharacterGlassShpkCount + => _characterGlassState.MaterialCount; + + public uint ModdedCharacterTransparencyShpkCount + => _characterTransparencyState.MaterialCount; + + public uint ModdedCharacterTattooShpkCount + => _characterTattooState.MaterialCount; + + public uint ModdedCharacterOcclusionShpkCount + => _characterOcclusionState.MaterialCount; + + public uint ModdedHairMaskShpkCount + => _hairMaskState.MaterialCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) @@ -64,7 +89,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; - _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[62], + + _skinState = new( + () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + _irisState = new(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new(() => _modelRenderer.CharacterTransparencyShaderPackage, () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new(() => _modelRenderer.CharacterTattooShaderPackage, () => _modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new(() => _modelRenderer.CharacterOcclusionShaderPackage, () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + _hairMaskState = new(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + + _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; @@ -78,14 +114,23 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _humanOnRenderMaterialHook.Dispose(); _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); - _moddedCharacterGlassShpkMaterials.Clear(); - _moddedSkinShpkMaterials.Clear(); - _moddedCharacterGlassShpkCount = 0; - _moddedSkinShpkCount = 0; + _hairMaskState.ClearMaterials(); + _characterOcclusionState.ClearMaterials(); + _characterTattooState.ClearMaterials(); + _characterTransparencyState.ClearMaterials(); + _characterGlassState.ClearMaterials(); + _irisState.ClearMaterials(); + _skinState.ClearMaterials(); } - public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() - => (Interlocked.Exchange(ref _skinSlowPathCallDelta, 0), Interlocked.Exchange(ref _characterGlassSlowPathCallDelta, 0)); + public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() + => (_skinState.GetAndResetSlowPathCallDelta(), + _irisState.GetAndResetSlowPathCallDelta(), + _characterGlassState.GetAndResetSlowPathCallDelta(), + _characterTransparencyState.GetAndResetSlowPathCallDelta(), + _characterTattooState.GetAndResetSlowPathCallDelta(), + _characterOcclusionState.GetAndResetSlowPathCallDelta(), + _hairMaskState.GetAndResetSlowPathCallDelta()); private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) { @@ -102,54 +147,99 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; + var shpkName = mtrl->ShpkNameSpan; + var shpkState = GetStateForHuman(shpkName) ?? GetStateForModelRenderer(shpkName); - if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) - if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) - Interlocked.Increment(ref _moddedSkinShpkCount); - - if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) - if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) - Interlocked.Increment(ref _moddedCharacterGlassShpkCount); + if (shpkState != null && shpk != shpkState.DefaultShaderPackage) + shpkState.TryAddMaterial(mtrlResourceHandle); } private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) { - if (_moddedSkinShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedSkinShpkCount); - - if (_moddedCharacterGlassShpkMaterials.TryRemove((nint)handle)) - Interlocked.Decrement(ref _moddedCharacterGlassShpkCount); + _skinState.TryRemoveMaterial(handle); + _irisState.TryRemoveMaterial(handle); + _characterGlassState.TryRemoveMaterial(handle); + _characterTransparencyState.TryRemoveMaterial(handle); + _characterTattooState.TryRemoveMaterial(handle); + _characterOcclusionState.TryRemoveMaterial(handle); + _hairMaskState.TryRemoveMaterial(handle); } + private ModdedShaderPackageState? GetStateForHuman(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHuman(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForHuman(ReadOnlySpan shpkName) + { + if (SkinShpkName.SequenceEqual(shpkName)) + return _skinState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForHuman() + => _skinState.MaterialCount; + + private ModdedShaderPackageState? GetStateForModelRenderer(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRenderer(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForModelRenderer(ReadOnlySpan shpkName) + { + if (IrisShpkName.SequenceEqual(shpkName)) + return _irisState; + + if (CharacterGlassShpkName.SequenceEqual(shpkName)) + return _characterGlassState; + + if (CharacterTransparencyShpkName.SequenceEqual(shpkName)) + return _characterTransparencyState; + + if (CharacterTattooShpkName.SequenceEqual(shpkName)) + return _characterTattooState; + + if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) + return _characterOcclusionState; + + if (HairMaskShpkName.SequenceEqual(shpkName)) + return _hairMaskState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForModelRenderer() + => _irisState.MaterialCount + _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || _moddedSkinShpkCount == 0) + if (!Enabled || GetTotalMaterialCountForHuman() == 0) return _humanOnRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var mtrlResource = material->MaterialResourceHandle; - if (!IsMaterialWithShpk(mtrlResource, SkinShpkName)) + var shpkState = GetStateForHuman(mtrlResource); + if (shpkState == null) return _humanOnRenderMaterialHook.Original(human, param); - Interlocked.Increment(ref _skinSlowPathCallDelta); + shpkState.IncrementSlowPathCallDelta(); // Performance considerations: // - This function is called from several threads simultaneously, hence the need for synchronization in the swapping path ; // - Function is called each frame for each material on screen, after culling, i.e. up to thousands of times a frame in crowded areas ; // - Swapping path is taken up to hundreds of times a frame. // At the time of writing, the lock doesn't seem to have a noticeable impact in either frame rate or CPU usage, but the swapping path shall still be avoided as much as possible. - lock (_skinLock) + lock (shpkState) { + var shpkReference = shpkState.ShaderPackageReference; try { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)mtrlResource->ShaderPackageResourceHandle; + *shpkReference = mtrlResource->ShaderPackageResourceHandle; return _humanOnRenderMaterialHook.Original(human, param); } finally { - _utility.Address->SkinShpkResource = (Structs.ResourceHandle*)_utility.DefaultSkinShpkResource; + *shpkReference = shpkState.DefaultShaderPackage; } } } @@ -158,27 +248,91 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. - if (!Enabled || _moddedCharacterGlassShpkCount == 0) + if (!Enabled || GetTotalMaterialCountForModelRenderer() == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); var mtrlResource = material->MaterialResourceHandle; - if (!IsMaterialWithShpk(mtrlResource, CharacterGlassShpkName)) + var shpkState = GetStateForModelRenderer(mtrlResource); + if (shpkState == null) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); - Interlocked.Increment(ref _characterGlassSlowPathCallDelta); + shpkState.IncrementSlowPathCallDelta(); // Same performance considerations as above. - lock (_characterGlassLock) + lock (shpkState) { + var shpkReference = shpkState.ShaderPackageReference; try { - *_modelRenderer.CharacterGlassShaderPackage = mtrlResource->ShaderPackageResourceHandle; + *shpkReference = mtrlResource->ShaderPackageResourceHandle; return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); } finally { - *_modelRenderer.CharacterGlassShaderPackage = _modelRenderer.DefaultCharacterGlassShaderPackage; + *shpkReference = shpkState.DefaultShaderPackage; } } } + + private sealed class ModdedShaderPackageState(ShaderPackageReferenceGetter referenceGetter, DefaultShaderPackageGetter defaultGetter) + { + // MaterialResourceHandle set + private readonly ConcurrentSet _materials = new(); + + // ConcurrentDictionary.Count uses a lock in its current implementation. + private uint _materialCount = 0; + + private ulong _slowPathCallDelta = 0; + + public uint MaterialCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => _materialCount; + } + + public ShaderPackageResourceHandle** ShaderPackageReference + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => referenceGetter(); + } + + public ShaderPackageResourceHandle* DefaultShaderPackage + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => defaultGetter(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void TryAddMaterial(nint mtrlResourceHandle) + { + if (_materials.TryAdd(mtrlResourceHandle)) + Interlocked.Increment(ref _materialCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void TryRemoveMaterial(Structs.ResourceHandle* handle) + { + if (_materials.TryRemove((nint)handle)) + Interlocked.Decrement(ref _materialCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void ClearMaterials() + { + _materials.Clear(); + _materialCount = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void IncrementSlowPathCallDelta() + => Interlocked.Increment(ref _slowPathCallDelta); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ulong GetAndResetSlowPathCallDelta() + => Interlocked.Exchange(ref _slowPathCallDelta, 0); + } + + private delegate ShaderPackageResourceHandle* DefaultShaderPackageGetter(); + + private delegate ShaderPackageResourceHandle** ShaderPackageReferenceGetter(); } diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index b268b395..10f3977f 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -9,6 +9,13 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } + public ShaderPackageResourceHandle** IrisShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.IrisShaderPackage, + }; + public ShaderPackageResourceHandle** CharacterGlassShaderPackage => Manager.Instance() switch { @@ -16,8 +23,46 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, }; + public ShaderPackageResourceHandle** CharacterTransparencyShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterTransparencyShaderPackage, + }; + + public ShaderPackageResourceHandle** CharacterTattooShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterTattooShaderPackage, + }; + + public ShaderPackageResourceHandle** CharacterOcclusionShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.CharacterOcclusionShaderPackage, + }; + + public ShaderPackageResourceHandle** HairMaskShaderPackage + => Manager.Instance() switch + { + null => null, + var renderManager => &renderManager->ModelRenderer.HairMaskShaderPackage, + }; + + public ShaderPackageResourceHandle* DefaultIrisShaderPackage { get; private set; } + public ShaderPackageResourceHandle* DefaultCharacterGlassShaderPackage { get; private set; } + public ShaderPackageResourceHandle* DefaultCharacterTransparencyShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultCharacterTattooShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultCharacterOcclusionShaderPackage { get; private set; } + + public ShaderPackageResourceHandle* DefaultHairMaskShaderPackage { get; private set; } + private readonly IFramework _framework; public ModelRenderer(IFramework framework) @@ -36,12 +81,42 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService var anyMissing = false; + if (DefaultIrisShaderPackage == null) + { + DefaultIrisShaderPackage = *IrisShaderPackage; + anyMissing |= DefaultIrisShaderPackage == null; + } + if (DefaultCharacterGlassShaderPackage == null) { DefaultCharacterGlassShaderPackage = *CharacterGlassShaderPackage; anyMissing |= DefaultCharacterGlassShaderPackage == null; } + if (DefaultCharacterTransparencyShaderPackage == null) + { + DefaultCharacterTransparencyShaderPackage = *CharacterTransparencyShaderPackage; + anyMissing |= DefaultCharacterTransparencyShaderPackage == null; + } + + if (DefaultCharacterTattooShaderPackage == null) + { + DefaultCharacterTattooShaderPackage = *CharacterTattooShaderPackage; + anyMissing |= DefaultCharacterTattooShaderPackage == null; + } + + if (DefaultCharacterOcclusionShaderPackage == null) + { + DefaultCharacterOcclusionShaderPackage = *CharacterOcclusionShaderPackage; + anyMissing |= DefaultCharacterOcclusionShaderPackage == null; + } + + if (DefaultHairMaskShaderPackage == null) + { + DefaultHairMaskShaderPackage = *HairMaskShaderPackage; + anyMissing |= DefaultHairMaskShaderPackage == null; + } + if (anyMissing) return; @@ -55,7 +130,12 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService if (!Ready) return; - *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + *HairMaskShaderPackage = DefaultHairMaskShaderPackage; + *CharacterOcclusionShaderPackage = DefaultCharacterOcclusionShaderPackage; + *CharacterTattooShaderPackage = DefaultCharacterTattooShaderPackage; + *CharacterTransparencyShaderPackage = DefaultCharacterTransparencyShaderPackage; + *CharacterGlassShaderPackage = DefaultCharacterGlassShaderPackage; + *IrisShaderPackage = DefaultIrisShaderPackage; } public void Dispose() diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a03f384..0c2581bf 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -177,6 +177,8 @@ public class DebugTab : Window, ITab, IUiService ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); + DrawShaderReplacementFixer(); + ImGui.NewLine(); DrawData(); ImGui.NewLine(); DrawResourceProblems(); @@ -711,27 +713,6 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Character Utility")) return; - var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; - if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) - _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; - - if (enableShaderReplacementFixer) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); - ImGui.SameLine(); - ImGui.TextUnformatted($"\u0394 Slow-Path Calls for skin.shpk: {slowPathCallDeltas.Skin}"); - ImGui.SameLine(); - ImGui.TextUnformatted($"characterglass.shpk: {slowPathCallDeltas.CharacterGlass}"); - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ImGui.TextUnformatted($"Materials with Modded skin.shpk: {_shaderReplacementFixer.ModdedSkinShpkCount}"); - ImGui.SameLine(); - ImGui.TextUnformatted($"characterglass.shpk: {_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); - } - using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) @@ -786,6 +767,80 @@ public class DebugTab : Window, ITab, IUiService } } + private void DrawShaderReplacementFixer() + { + if (!ImGui.CollapsingHeader("Shader Replacement Fixer")) + return; + + var enableShaderReplacementFixer = _shaderReplacementFixer.Enabled; + if (ImGui.Checkbox("Enable Shader Replacement Fixer", ref enableShaderReplacementFixer)) + _shaderReplacementFixer.Enabled = enableShaderReplacementFixer; + + if (!enableShaderReplacementFixer) + return; + + using var table = Table("##ShaderReplacementFixer", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var slowPathCallDeltas = _shaderReplacementFixer.GetAndResetSlowPathCallDeltas(); + + ImGui.TableSetupColumn("Shader Package Name", ImGuiTableColumnFlags.WidthStretch, 0.6f); + ImGui.TableSetupColumn("Materials with Modded ShPk", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableSetupColumn("\u0394 Slow-Path Calls", ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImGui.TableHeadersRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("skin.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("iris.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterglass.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterGlassShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertattoo.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterocclusion.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterOcclusionShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("hairmask.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}"); + } + /// Draw information about the resident resource files. private unsafe void DrawDebugResidentResources() { From 68135f3757eb4de0cd7ce83f37c7d7a6544656fd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jul 2024 14:39:03 +0200 Subject: [PATCH 204/865] Update Gamedata --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 10 ++-- .../Interop/Hooks/Meta/GetEqpIndirect2.cs | 1 - Penumbra/Interop/Hooks/Meta/GmpHook.cs | 47 ++++--------------- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 ++-- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 ++-- .../Interop/Hooks/Objects/CopyCharacter.cs | 5 +- .../Hooks/Objects/CreateCharacterBase.cs | 6 +-- .../Interop/Structs/CharacterUtilityData.cs | 10 ++-- Penumbra/Interop/Structs/MetaIndex.cs | 13 +++-- 11 files changed, 40 insertions(+), 75 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19923f8d..62f6acfb 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19923f8d5649f11edcfae710c26d6273cf2e9d62 +Subproject commit 62f6acfb0f9e31b2907c02a270d723bfff18b390 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index ed4eb669..9dd8d74f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -4,11 +4,11 @@ public static class HookSettings { public const bool AllHooks = true; - public const bool ObjectHooks = false && AllHooks; + public const bool ObjectHooks = true && AllHooks; public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = false && AllHooks; - public const bool MetaEntryHooks = false && AllHooks; - public const bool MetaParentHooks = false && AllHooks; + public const bool ResourceHooks = true && AllHooks; + public const bool MetaEntryHooks = true && AllHooks; + public const bool MetaParentHooks = true && AllHooks; public const bool VfxIdentificationHooks = false && AllHooks; - public const bool PostProcessingHooks = false && AllHooks; + public const bool PostProcessingHooks = true && AllHooks; } diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs index 3767c4a2..e90674a8 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs @@ -1,6 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.Collections; using Penumbra.GameData; using Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 329a8beb..12b221d9 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using Lumina.Data.Parsing.Uld; using OtterGui.Services; using Penumbra.GameData; using Penumbra.Interop.PathResolving; @@ -8,12 +10,10 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class GmpHook : FastHook, IDisposable { - public delegate nint Delegate(nint gmpResource, uint dividedHeadId); + public delegate ulong Delegate(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId); private readonly MetaState _metaState; - private static readonly Finalizer StablePointer = new(); - public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; @@ -21,50 +21,21 @@ public unsafe class GmpHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - /// - /// This function returns a pointer to the correct block in the GMP file, if it exists - cf. . - /// To work around this, we just have a single stable ulong accessible and offset the pointer to this by the required distance, - /// which is defined by the modulo of the original ID and the block size, if we return our own custom gmp entry. - /// - private nint Detour(nint gmpResource, uint dividedHeadId) + private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) { - nint ret; + ulong ret; if (_metaState.GmpCollection.TryPeek(out var collection) && collection.Collection is { Valid: true, ModCollection.MetaCache: { } cache } && cache.Gmp.TryGetValue(new GmpIdentifier(collection.Id), out var entry)) - { - if (entry.Entry.Enabled) - { - *StablePointer.Pointer = entry.Entry.Value; - // This function already gets the original ID divided by the block size, so we can compute the modulo with a single multiplication and addition. - // We then go backwards from our pointer because this gets added by the calling functions. - ret = (nint)(StablePointer.Pointer - (collection.Id.Id - dividedHeadId * ExpandedEqpGmpBase.BlockSize)); - } - else - { - ret = nint.Zero; - } - } + ret = (*outputEntry) = entry.Entry.Enabled ? entry.Entry.Value : 0ul; else - { - ret = Task.Result.Original(gmpResource, dividedHeadId); - } + ret = Task.Result.Original(characterUtility, outputEntry, setId); - Penumbra.Log.Excessive($"[GetGmpFlags] Invoked on 0x{gmpResource:X} with {dividedHeadId}, returned {ret:X10}."); + Penumbra.Log.Excessive( + $"[GetGmpFlags] Invoked on 0x{(ulong)characterUtility:X} for {setId} with 0x{(ulong)outputEntry:X} (={*outputEntry:X}), returned {ret:X10}."); return ret; } - /// Allocate and clean up our single stable ulong pointer. - private class Finalizer - { - public readonly ulong* Pointer = (ulong*)Marshal.AllocHGlobal(8); - - ~Finalizer() - { - Marshal.FreeHGlobal((nint)Pointer); - } - } - public void Dispose() => _metaState.Config.ModsEnabled -= Toggle; } diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index 79e7f6a6..c1803745 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[58], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index eb8a8a37..e08dc393 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -10,8 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public unsafe class RspBustHook : FastHook, IDisposable { - public delegate float* Delegate(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, - byte bustSize); + public delegate float* Delegate(nint cmpResource, float* storage, SubRace race, byte gender, byte bodyType, byte bustSize); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -24,7 +23,7 @@ public unsafe class RspBustHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private float* Detour(nint cmpResource, float* storage, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte bustSize) + private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) { if (gender == 0) { @@ -38,7 +37,6 @@ public unsafe class RspBustHook : FastHook, IDisposable if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); storage[0] = GetValue(0, RspAttribute.BustMinX, RspAttribute.BustMaxX); storage[1] = GetValue(1, RspAttribute.BustMinY, RspAttribute.BustMaxY); @@ -58,11 +56,11 @@ public unsafe class RspBustHook : FastHook, IDisposable } else { - ret = Task.Result.Original(cmpResource, storage, race, gender, isSecondSubRace, bodyType, bustSize); + ret = Task.Result.Original(cmpResource, storage, clan, gender, bodyType, bustSize); } Penumbra.Log.Excessive( - $"[GetRspBust] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); + $"[GetRspBust] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {bustSize}, returned {storage[0]}, {storage[1]}, {storage[2]}."); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index f8f9e51e..20e3c939 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Hooks.Meta; public class RspHeightHook : FastHook, IDisposable { - public delegate float Delegate(nint cmpResource, Race clan, byte gender, byte isSecondSubRace, byte bodyType, byte height); + public delegate float Delegate(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height); private readonly MetaState _metaState; private readonly MetaFileManager _metaFileManager; @@ -23,7 +23,7 @@ public class RspHeightHook : FastHook, IDisposable _metaState.Config.ModsEnabled += Toggle; } - private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte height) + private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) { float scale; if (bodyType < 2 @@ -36,7 +36,6 @@ public class RspHeightHook : FastHook, IDisposable if (height > 100) height = 0; - var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 ? (new RspIdentifier(clan, RspAttribute.MaleMinSize), new RspIdentifier(clan, RspAttribute.MaleMaxSize)) : (new RspIdentifier(clan, RspAttribute.FemaleMinSize), new RspIdentifier(clan, RspAttribute.FemaleMaxSize)); @@ -68,11 +67,11 @@ public class RspHeightHook : FastHook, IDisposable } else { - scale = Task.Result.Original(cmpResource, race, gender, isSecondSubRace, bodyType, height); + scale = Task.Result.Original(cmpResource, clan, gender, bodyType, height); } Penumbra.Log.Excessive( - $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {race}, {(Gender)(gender + 1)}, {isSecondSubRace == 1}, {bodyType}, {height}, returned {scale}."); + $"[GetRspHeight] Invoked on 0x{cmpResource:X} with {clan}, {(Gender)(gender + 1)}, {bodyType}, {height}, returned {scale}."); return scale; } diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index 663209ae..d81043c8 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -9,7 +9,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr + /// CutsceneService = 0, } @@ -38,8 +38,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtrOwnerObject; Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}."); Invoke(character, source); return _task.Result.Original(target, source, unk); diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index 56b3d853..f00a9984 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -10,7 +10,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// MetaState = 0, } @@ -64,10 +64,10 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr + /// DrawObjectState = 0, - /// + /// MetaState = 0, } } diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 22150cc1..d33da477 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -6,16 +6,16 @@ namespace Penumbra.Interop.Structs; public unsafe struct CharacterUtilityData { public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 72; - public const int IndexDecalTex = 73; - public const int IndexSkinShpk = 76; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexSkinShpk = 83; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 87; + public const int TotalNumResources = 89; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) @@ -36,7 +36,7 @@ public unsafe struct CharacterUtilityData 1301 => accessory ? MetaIndex.Eqdp1301Acc : MetaIndex.Eqdp1301, 1401 => accessory ? MetaIndex.Eqdp1401Acc : MetaIndex.Eqdp1401, 1501 => accessory ? MetaIndex.Eqdp1501Acc : MetaIndex.Eqdp1501, - //1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, Female Hrothgar + 1601 => accessory ? MetaIndex.Eqdp1601Acc : MetaIndex.Eqdp1601, 1701 => accessory ? MetaIndex.Eqdp1701Acc : MetaIndex.Eqdp1701, 1801 => accessory ? MetaIndex.Eqdp1801Acc : MetaIndex.Eqdp1801, 0104 => accessory ? MetaIndex.Eqdp0104Acc : MetaIndex.Eqdp0104, diff --git a/Penumbra/Interop/Structs/MetaIndex.cs b/Penumbra/Interop/Structs/MetaIndex.cs index 65302264..2ec5fce4 100644 --- a/Penumbra/Interop/Structs/MetaIndex.cs +++ b/Penumbra/Interop/Structs/MetaIndex.cs @@ -4,6 +4,7 @@ namespace Penumbra.Interop.Structs; public enum MetaIndex : int { Eqp = 0, + Evp = 1, Gmp = 2, Eqdp0101 = 3, @@ -21,9 +22,8 @@ public enum MetaIndex : int Eqdp1301, Eqdp1401, Eqdp1501, - - //Eqdp1601, // TODO: female Hrothgar - Eqdp1701 = Eqdp1501 + 2, + Eqdp1601, + Eqdp1701, Eqdp1801, Eqdp0104, Eqdp0204, @@ -51,9 +51,8 @@ public enum MetaIndex : int Eqdp1301Acc, Eqdp1401Acc, Eqdp1501Acc, - - //Eqdp1601Acc, // TODO: female Hrothgar - Eqdp1701Acc = Eqdp1501Acc + 2, + Eqdp1601Acc, + Eqdp1701Acc, Eqdp1801Acc, Eqdp0104Acc, Eqdp0204Acc, @@ -66,7 +65,7 @@ public enum MetaIndex : int Eqdp9104Acc, Eqdp9204Acc, - HumanCmp = 64, + HumanCmp = 71, FaceEst, HairEst, HeadEst, From 4f0f3721a6296e0fad1c816c06e42d0ac3cf0de0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jul 2024 16:16:58 +0200 Subject: [PATCH 205/865] Update animation hooks. --- Penumbra/Interop/Hooks/Animation/Dismount.cs | 13 ++++---- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 8 ++--- .../Hooks/Animation/LoadCharacterSound.cs | 16 ++++++---- .../Hooks/Animation/LoadTimelineResources.cs | 32 +++++++++---------- Penumbra/Interop/Hooks/HookSettings.cs | 2 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 30 +++++++++-------- .../Hooks/ResourceLoading/TexMdlService.cs | 18 +++++++++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/Interop/Structs/ClipScheduler.cs | 4 ++- Penumbra/Interop/Structs/StructExtensions.cs | 12 +++---- Penumbra/UI/Tabs/Debug/DebugTab.cs | 7 ++-- 11 files changed, 83 insertions(+), 61 deletions(-) diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs index 523e750c..034011e7 100644 --- a/Penumbra/Interop/Hooks/Animation/Dismount.cs +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.GameData; @@ -18,26 +19,26 @@ public sealed unsafe class Dismount : FastHook Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, HookSettings.VfxIdentificationHooks); } - public delegate void Delegate(nint a1, nint a2); + public delegate void Delegate(MountContainer* a1, nint a2); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Detour(nint a1, nint a2) + private void Detour(MountContainer* a1, nint a2) { - Penumbra.Log.Excessive($"[Dismount] Invoked on {a1:X} with {a2:X}."); - if (a1 == nint.Zero) + Penumbra.Log.Excessive($"[Dismount] Invoked on 0x{(nint)a1:X} with {a2:X}."); + if (a1 == null) { Task.Result.Original(a1, a2); return; } - var gameObject = *(GameObject**)(a1 + 8); + var gameObject = a1->OwnerObject; if (gameObject == null) { Task.Result.Original(a1, a2); return; } - var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true)); + var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*) gameObject, true)); Task.Result.Original(a1, a2); _state.RestoreAnimationData(last); } diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index 0f51157c..cfab29d3 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -29,13 +29,13 @@ public sealed unsafe class LoadAreaVfx : FastHook private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3) { var newData = caster != null - ? _collectionResolver.IdentifyCollection(caster, true) - : ResolveData.Invalid; + ? _collectionResolver.IdentifyCollection(caster, true) + : ResolveData.Invalid; var last = _state.SetAnimationData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx); - var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); - Penumbra.Log.Excessive( + var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); + Penumbra.Log.Information( $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); _state.RestoreAnimationData(last); return ret; diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index ed04880e..8d1096d2 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -16,23 +16,25 @@ public sealed unsafe class LoadCharacterSound : FastHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, HookSettings.VfxIdentificationHooks); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, + HookSettings.VfxIdentificationHooks); } - public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); + public delegate nint Delegate(VfxContainer* container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) + private nint Detour(VfxContainer* container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7) { - var character = *(GameObject**)(container + 8); + var character = (GameObject*)container->OwnerObject; var newData = _collectionResolver.IdentifyCollection(character, true); var last = _state.SetSoundData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterSound); var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7); - Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}."); + Penumbra.Log.Excessive( + $"[Load Character Sound] Invoked with {(nint)container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}."); _state.RestoreSoundData(last); return ret; } diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 4e9037bd..8bb14db6 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Base; using OtterGui.Services; using Penumbra.Collections; using Penumbra.GameData; @@ -25,45 +26,44 @@ public sealed unsafe class LoadTimelineResources : FastHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); + _conditions = conditions; + _objects = objects; + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); } - public delegate ulong Delegate(nint timeline); + public delegate ulong Delegate(SchedulerTimeline* timeline); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(nint timeline) + private ulong Detour(SchedulerTimeline* timeline) { - Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {timeline:X}."); + Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {(nint)timeline:X}."); // Do not check timeline loading in cutscenes. if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78]) return Task.Result.Original(timeline); var newData = GetDataFromTimeline(_objects, _collectionResolver, timeline); - var last = _state.SetAnimationData(newData); - + var last = _state.SetAnimationData(newData); + #if false // This is called far too often and spams the log too much. _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadTimelineResources); #endif - var ret = Task.Result.Original(timeline); + var ret = Task.Result.Original(timeline); _state.RestoreAnimationData(last); return ret; } /// Use timelines vfuncs to obtain the associated game object. - public static ResolveData GetDataFromTimeline(ObjectManager objects, CollectionResolver resolver, nint timeline) + public static ResolveData GetDataFromTimeline(ObjectManager objects, CollectionResolver resolver, SchedulerTimeline* timeline) { try { - if (timeline != nint.Zero) + if (timeline != null) { - var getGameObjectIdx = ((delegate* unmanaged**)timeline)[0][Offsets.GetGameObjectIdxVfunc]; - var idx = getGameObjectIdx(timeline); + var idx = timeline->GetOwningGameObjectIndex(); if (idx >= 0 && idx < objects.TotalCount) { var obj = objects[idx]; @@ -73,7 +73,7 @@ public sealed unsafe class LoadTimelineResources : FastHook Load a resource for a given path and a specific collection. @@ -80,10 +80,10 @@ public unsafe class ResourceLoader : IDisposable, IService public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceRequested -= ResourceHandler; _resources.ResourceHandleIncRef -= IncRefProtection; _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _fileReadService.ReadSqPack -= ReadSqPackDetour; } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, @@ -94,6 +94,9 @@ public unsafe class ResourceLoader : IDisposable, IService CompareHash(ComputeHash(path.Path, parameters), hash, path); + if (path.ToString() == "vfx/common/eff/abi_cnj022g.avfx") + ; + // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) @@ -112,7 +115,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; - path = p; + path = p; returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } @@ -121,7 +124,8 @@ public unsafe class ResourceLoader : IDisposable, IService { if (fileDescriptor->ResourceHandle == null) { - Penumbra.Log.Error("[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor."); + Penumbra.Log.Verbose( + $"[ResourceLoader] Failure to load file from SqPack: invalid File Descriptor: {Marshal.PtrToStringUni((nint)(&fileDescriptor->Utf16FileName))}"); return; } @@ -140,12 +144,12 @@ public unsafe class ResourceLoader : IDisposable, IService } var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); - fileDescriptor->ResourceHandle->FileNameData = path.Path; + fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. - fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; + fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; fileDescriptor->ResourceHandle->FileNameLength = gamePath.Path.Length; } @@ -165,7 +169,7 @@ public unsafe class ResourceLoader : IDisposable, IService // Ensure that the file descriptor has its wchar_t array on aligned boundary even if it has to be odd. var fd = stackalloc char[0x11 + 0x0B + 14]; fileDescriptor->FileDescriptor = (byte*)fd + 1; - CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); + CreateFileWHook.WritePtr(fd + 0x11, gamePath.Path, gamePath.Length); CreateFileWHook.WritePtr(&fileDescriptor->Utf16FileName, gamePath.Path, gamePath.Length); // Use the SE ReadFile function. @@ -206,7 +210,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; _incMode.Value = true; - returnValue = _resources.IncRef(handle); + returnValue = _resources.IncRef(handle); _incMode.Value = false; } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 793d15af..80ba5cb9 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -87,6 +87,11 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private readonly ThreadLocal _texReturnData = new(() => default); + private delegate void UpdateCategoryDelegate(TextureResourceHandle* resourceHandle); + + [Signature(Sigs.TexHandleUpdateCategory)] + private readonly UpdateCategoryDelegate _updateCategory = null!; + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models. @@ -99,9 +104,14 @@ public unsafe class TexMdlService : IDisposable, IRequiredService return CustomFileFlag; if (_customTexCrc.Contains(crc64)) + { _texReturnData.Value = true; + return nint.Zero; + } - return _checkFileStateHook.Original(ptr, crc64); + var ret = _checkFileStateHook.Original(ptr, crc64); + Penumbra.Log.Excessive($"[CheckFileState] Called on 0x{ptr:X} with CRC {crc64:X16}, returned 0x{ret:X}."); + return ret; } private delegate byte LoadTexFileLocalDelegate(TextureResourceHandle* handle, int unk1, SeFileDescriptor* unk2, bool unk3); @@ -118,7 +128,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - [Signature(Sigs.TexResourceHandleOnLoad, DetourName = nameof(OnLoadDetour))] + [Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnLoadDetour))] private readonly Hook _textureOnLoadHook = null!; private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) @@ -129,7 +139,9 @@ public unsafe class TexMdlService : IDisposable, IRequiredService // Function failed on a replaced texture, call local. _texReturnData.Value = false; - return _loadTexFileLocal(handle, _lodService.GetLod(handle), descriptor, unk2 != 0); + ret = _loadTexFileLocal(handle, _lodService.GetLod(handle), descriptor, unk2 != 0); + _updateCategory(handle); + return ret; } private delegate byte LoadMdlFileExternPrototype(ResourceHandle* handle, nint unk1, bool unk2, nint unk3); diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 810c946d..96125df2 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -124,7 +124,7 @@ public class ResourceTree // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; - var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain1, weapon->Stain2)); + var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1)); var weaponType = weapon->SecondaryId; var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType); diff --git a/Penumbra/Interop/Structs/ClipScheduler.cs b/Penumbra/Interop/Structs/ClipScheduler.cs index 44a905b8..8270e0f2 100644 --- a/Penumbra/Interop/Structs/ClipScheduler.cs +++ b/Penumbra/Interop/Structs/ClipScheduler.cs @@ -1,3 +1,5 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Base; + namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] @@ -7,5 +9,5 @@ public unsafe struct ClipScheduler public nint* VTable; [FieldOffset(0x38)] - public nint SchedulerTimeline; + public SchedulerTimeline* SchedulerTimeline; } diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 3cd87424..fc8b1c3d 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -9,37 +9,37 @@ internal static class StructExtensions public static unsafe ByteString AsByteString(in this StdString str) => ByteString.FromSpanUnsafe(str.AsSpan(), true); - public static ByteString ResolveEidPathAsByteString(in this CharacterBase character) + public static ByteString ResolveEidPathAsByteString(ref this CharacterBase character) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); } - public static ByteString ResolveImcPathAsByteString(in this CharacterBase character, uint slotIndex) + public static ByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex)); } - public static ByteString ResolveMdlPathAsByteString(in this CharacterBase character, uint slotIndex) + public static ByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex)); } - public static unsafe ByteString ResolveMtrlPathAsByteString(in this CharacterBase character, uint slotIndex, byte* mtrlFileName) + public static unsafe ByteString 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 ByteString ResolveSklbPathAsByteString(in this CharacterBase character, uint partialSkeletonIndex) + public static ByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex)); } - public static ByteString ResolveSkpPathAsByteString(in this CharacterBase character, uint partialSkeletonIndex) + public static ByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9a03f384..180049bc 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -430,7 +430,7 @@ public class DebugTab : Window, ITab, IUiService foreach (var obj in _objects) { - ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero ? string.Empty @@ -482,14 +482,15 @@ public class DebugTab : Window, ITab, IUiService { var gameObject = (GameObject*)gameObjectPtr; ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{drawObject:X}"); + + ImGuiUtil.CopyOnClickSelectable($"0x{drawObject:X}"); ImGui.TableNextColumn(); ImGui.TextUnformatted(gameObject->ObjectIndex.ToString()); ImGui.TableNextColumn(); ImGui.TextUnformatted(child ? "Child" : "Main"); ImGui.TableNextColumn(); var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString()); - ImGui.TextUnformatted(address); + ImGuiUtil.CopyOnClickSelectable(address); ImGui.TableNextColumn(); ImGui.TextUnformatted(name); ImGui.TableNextColumn(); From 0d939b12f4381a8dc4e6c96d977fdbd03348424f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jul 2024 16:17:12 +0200 Subject: [PATCH 206/865] Add model update button. --- Penumbra/UI/AdvancedWindow/FileEditor.cs | 15 +++++++++------ .../AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 1 - .../UI/AdvancedWindow/ModEditWindow.Models.cs | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index eeb94c71..2c6ac170 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -207,12 +207,15 @@ public class FileEditor( var canSave = _changed && _currentFile is { Valid: true }; if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, $"Save the selected {fileType} file with all changes applied. This is not revertible.", !canSave)) - { - compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); - if (owner.Mod != null) - communicator.ModFileChanged.Invoke(owner.Mod, _currentPath); - _changed = false; - } + SaveFile(); + } + + public void SaveFile() + { + compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + if (owner.Mod != null) + communicator.ModFileChanged.Invoke(owner.Mod, _currentPath); + _changed = false; } private void ResetButton() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 72ab37b2..b05bcac2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,4 +1,3 @@ -using Lumina.Data.Parsing; using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bbed64b7..de088736 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -4,6 +4,7 @@ using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; @@ -67,6 +68,7 @@ public partial class ModEditWindow { var ret = tab.Dirty; var data = UpdateFile(tab.Mdl, ret, disabled); + DrawVersionUpdate(tab, disabled); DrawImportExport(tab, disabled); ret |= DrawModelMaterialDetails(tab, disabled); @@ -80,6 +82,19 @@ public partial class ModEditWindow return !disabled && ret; } + private void DrawVersionUpdate(MdlTab tab, bool disabled) + { + if (disabled || tab.Mdl.Version is not MdlFile.V5) + return; + + if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return; + + tab.Mdl.ConvertV5ToV6(); + _modelTab.SaveFile(); + } + private void DrawImportExport(MdlTab tab, bool disabled) { if (!ImGui.CollapsingHeader("Import / Export")) From 56e284a99eedb73f2053d4e961d714edcbfc1937 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jul 2024 14:55:49 +0200 Subject: [PATCH 207/865] Add some migration things. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Configuration.cs | 2 + Penumbra/Import/TexToolsImport.cs | 33 +- Penumbra/Import/TexToolsImporter.Archives.cs | 26 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 23 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 2 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 3 - .../ResolveContext.PathResolution.cs | 50 +-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 6 +- Penumbra/Mods/Manager/ModImportManager.cs | 5 +- Penumbra/Services/MigrationManager.cs | 287 ++++++++++++++++++ .../AdvancedWindow/ModEditWindow.Materials.cs | 16 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 5 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 4 +- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 121 ++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 8 +- 17 files changed, 515 insertions(+), 80 deletions(-) create mode 100644 Penumbra/Services/MigrationManager.cs create mode 100644 Penumbra/UI/Classes/MigrationSectionDrawer.cs diff --git a/OtterGui b/OtterGui index c2738e1d..89b3b951 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c2738e1d42974cddbe5a31238c6ed236a831d17d +Subproject commit 89b3b9513f9b4989045517a452ef971e24377203 diff --git a/Penumbra.GameData b/Penumbra.GameData index 62f6acfb..b5eb074d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 62f6acfb0f9e31b2907c02a270d723bfff18b390 +Subproject commit b5eb074d80a4aaa2703a3124d2973a7fa08046ad diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 49aecfdc..8d0f7fd8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -99,6 +99,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; + public bool MigrateImportedModelsToV6 { get; set; } = false; + public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; public bool KeepDefaultMetaChanges { get; set; } = false; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index bb006d8d..ba089662 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -3,6 +3,7 @@ using OtterGui.Compression; using Penumbra.Import.Structs; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; +using Penumbra.Services; using FileMode = System.IO.FileMode; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry; @@ -27,24 +28,26 @@ public partial class TexToolsImporter : IDisposable public ImporterState State { get; private set; } public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods; - private readonly Configuration _config; - private readonly ModEditor _editor; - private readonly ModManager _modManager; - private readonly FileCompactor _compactor; + private readonly Configuration _config; + private readonly ModEditor _editor; + private readonly ModManager _modManager; + private readonly FileCompactor _compactor; + private readonly MigrationManager _migrationManager; public TexToolsImporter(int count, IEnumerable modPackFiles, Action handler, - Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor) + Configuration config, ModEditor editor, ModManager modManager, FileCompactor compactor, MigrationManager migrationManager) { - _baseDirectory = modManager.BasePath; - _tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName); - _modPackFiles = modPackFiles; - _config = config; - _editor = editor; - _modManager = modManager; - _compactor = compactor; - _modPackCount = count; - ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count); - _token = _cancellation.Token; + _baseDirectory = modManager.BasePath; + _tmpFile = Path.Combine(_baseDirectory.FullName, TempFileName); + _modPackFiles = modPackFiles; + _config = config; + _editor = editor; + _modManager = modManager; + _compactor = compactor; + _migrationManager = migrationManager; + _modPackCount = count; + ExtractedMods = new List<(FileInfo, DirectoryInfo?, Exception?)>(count); + _token = _cancellation.Token; Task.Run(ImportFiles, _token) .ContinueWith(_ => CloseStreams(), TaskScheduler.Default) .ContinueWith(_ => diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 57313ab1..a51dbc61 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -2,6 +2,7 @@ using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; +using Penumbra.GameData.Files; using Penumbra.Import.Structs; using Penumbra.Mods; using SharpCompress.Archives; @@ -9,12 +10,19 @@ using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; using SharpCompress.Readers; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace Penumbra.Import; public partial class TexToolsImporter { + private static readonly ExtractionOptions _extractionOptions = new() + { + ExtractFullPath = true, + Overwrite = true, + }; + /// /// Extract regular compressed archives that are folders containing penumbra-formatted mods. /// The mod has to either contain a meta.json at top level, or one folder deep. @@ -45,11 +53,7 @@ public partial class TexToolsImporter Penumbra.Log.Information($" -> Importing {archive.Type} Archive."); _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetRandomFileName(), _config.ReplaceNonAsciiOnImport, true); - var options = new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true, - }; + State = ImporterState.ExtractingModFiles; _currentFileIdx = 0; @@ -86,7 +90,7 @@ public partial class TexToolsImporter } else { - reader.WriteEntryToDirectory(_currentModDirectory.FullName, options); + HandleFileMigrations(reader); } ++_currentFileIdx; @@ -114,6 +118,16 @@ public partial class TexToolsImporter } + private void HandleFileMigrations(IReader reader) + { + switch (Path.GetExtension(reader.Entry.Key)) + { + case ".mdl": + _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); + break; + } + } + // Search the archive for the meta.json file which needs to exist. private static string FindArchiveModMeta(IArchive archive, out bool leadDir) { diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 2b45ecbe..ba294353 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -253,25 +253,12 @@ public partial class TexToolsImporter extractedFile.Directory?.Create(); - if (extractedFile.FullName.EndsWith(".mdl")) - ProcessMdl(data.Data); + data.Data = Path.GetExtension(extractedFile.FullName) switch + { + ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), + _ => data.Data, + }; _compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token); } - - private static void ProcessMdl(byte[] mdl) - { - const int modelHeaderLodOffset = 22; - - // Model file header LOD num - mdl[64] = 1; - - // Model header LOD num - var stackSize = BitConverter.ToUInt32(mdl, 4); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32(mdl, (int)stringsLengthOffset); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - mdl[modelHeaderStart + modelHeaderLodOffset] = 1; - } } diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index cfab29d3..48dc0078 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -35,7 +35,7 @@ public sealed unsafe class LoadAreaVfx : FastHook var last = _state.SetAnimationData(newData); _crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx); var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}."); _state.RestoreAnimationData(last); return ret; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index af637768..195a8b9e 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -94,9 +94,6 @@ public unsafe class ResourceLoader : IDisposable, IService CompareHash(ComputeHash(path.Path, parameters), hash, path); - if (path.ToString() == "vfx/common/eff/abi_cnj022g.avfx") - ; - // If no replacements are being made, we still want to be able to trigger the event. var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 4dfefd96..72cb1681 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -81,11 +81,12 @@ internal partial record ResolveContext // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { - ModelType.Human when SlotIndex < 10 && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), - ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), - ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), - ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), - _ => ResolveMaterialPathNative(mtrlFileName), + ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' + => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), + ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), + _ => ResolveMaterialPathNative(mtrlFileName), }; } @@ -96,7 +97,7 @@ internal partial record ResolveContext var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } @@ -126,7 +127,7 @@ internal partial record ResolveContext WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); if (weaponPosition >= 0) @@ -145,7 +146,7 @@ internal partial record ResolveContext var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[260]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; } @@ -166,7 +167,8 @@ internal partial record ResolveContext return entry.MaterialId; } - private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, ReadOnlySpan mtrlFileName) + private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, + ReadOnlySpan mtrlFileName) { var modelPosition = modelPath.IndexOf("/model/"u8); if (modelPosition < 0) @@ -187,8 +189,8 @@ internal partial record ResolveContext { for (var i = destination.Length; i-- > 0;) { - destination[i] = (byte)('0' + number % 10); - number /= 10; + destination[i] = (byte)('0' + number % 10); + number /= 10; } } @@ -197,13 +199,17 @@ internal partial record ResolveContext ByteString? path; try { + Penumbra.Log.Information($"{(nint)CharacterBase:X} {ModelType} {SlotIndex} 0x{(ulong)mtrlFileName:X}"); + Penumbra.Log.Information($"{new ByteString(mtrlFileName)}"); path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); } catch (AccessViolationException) { - Penumbra.Log.Error($"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); + Penumbra.Log.Error( + $"Access violation during attempt to resolve material path\nDraw object: {(nint)CharacterBase:X} (of type {ModelType})\nSlot index: {SlotIndex}\nMaterial file name: {(nint)mtrlFileName:X} ({new string((sbyte*)mtrlFileName)})"); return Utf8GamePath.Empty; } + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -235,30 +241,23 @@ internal partial record ResolveContext var characterRaceCode = (GenderRace)human->RaceSexId; switch (partialSkeletonIndex) { - case 0: - return (characterRaceCode, "base", 1); + case 0: return (characterRaceCode, "base", 1); case 1: var faceId = human->FaceId; var tribe = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.Tribe]; var modelType = human->Customize[(int)Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex.ModelType]; if (faceId < 201) - { faceId -= tribe switch { 0xB when modelType == 4 => 100, 0xE | 0xF => 100, _ => 0, }; - } return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Face, faceId); - case 2: - return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId); - case 3: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head); - case 4: - return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body); - default: - return (0, string.Empty, 0); + case 2: return ResolveHumanExtraSkeletonData(characterRaceCode, EstType.Hair, human->HairId); + case 3: return ResolveHumanEquipmentSkeletonData(EquipSlot.Head, EstType.Head); + case 4: return ResolveHumanEquipmentSkeletonData(EquipSlot.Body, EstType.Body); + default: return (0, string.Empty, 0); } } @@ -269,7 +268,8 @@ internal partial record ResolveContext return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); } - private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary) + private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, + PrimaryId primary) { var metaCache = Global.Collection.MetaCache; var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 96125df2..6663fb40 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -73,11 +73,11 @@ public class ResourceTree var genericContext = globalContext.CreateContext(model); - for (var i = 0; i < model->SlotCount; ++i) + for (var i = 0u; i < model->SlotCount; ++i) { var slotContext = i < equipment.Length - ? globalContext.CreateContext(model, (uint)i, ((uint)i).ToEquipSlot(), equipment[i]) - : globalContext.CreateContext(model, (uint)i); + ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + : globalContext.CreateContext(model, i); var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 39a53bb9..d984d374 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -3,10 +3,11 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.Import; using Penumbra.Mods.Editor; +using Penumbra.Services; namespace Penumbra.Mods.Manager; -public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor) : IDisposable, IService +public class ModImportManager(ModManager modManager, Configuration config, ModEditor modEditor, MigrationManager migrationManager) : IDisposable, IService { private readonly ConcurrentQueue _modsToUnpack = new(); @@ -42,7 +43,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd if (files.Length == 0) return; - _import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor); + _import = new TexToolsImporter(files.Length, files, AddNewMod, config, modEditor, modManager, modEditor.Compactor, migrationManager); } public bool Importing diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs new file mode 100644 index 00000000..a4b80656 --- /dev/null +++ b/Penumbra/Services/MigrationManager.cs @@ -0,0 +1,287 @@ +using Dalamud.Interface.ImGuiNotification; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData.Files; +using SharpCompress.Common; +using SharpCompress.Readers; + +namespace Penumbra.Services; + +public class MigrationManager(Configuration config) : IService +{ + private Task? _currentTask; + private CancellationTokenSource? _source; + + public bool HasCleanUpTask { get; private set; } + public bool HasMigrationTask { get; private set; } + public bool HasRestoreTask { get; private set; } + + public bool IsMigrationTask { get; private set; } + public bool IsRestorationTask { get; private set; } + public bool IsCleanupTask { get; private set; } + + + public int Restored { get; private set; } + public int RestoreFails { get; private set; } + + public int CleanedUp { get; private set; } + + public int CleanupFails { get; private set; } + + public int Migrated { get; private set; } + + public int Unchanged { get; private set; } + + public int Failed { get; private set; } + + public bool IsRunning + => _currentTask is { IsCompleted: false }; + + /// Writes or migrates a .mdl file during extraction from a regular archive. + public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) + { + if (!config.MigrateImportedModelsToV6) + { + reader.WriteEntryToDirectory(directory, options); + return; + } + + var path = Path.Combine(directory, reader.Entry.Key); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + using var b = new BinaryReader(s); + var version = b.ReadUInt32(); + if (version == MdlFile.V5) + { + var data = s.ToArray(); + var mdl = new MdlFile(data); + MigrateModel(path, mdl, false); + Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); + } + else + { + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } + } + + public void CleanBackups(string path) + { + if (IsRunning) + return; + + _source = new CancellationTokenSource(); + var token = _source.Token; + _currentTask = Task.Run(() => + { + HasCleanUpTask = true; + IsCleanupTask = true; + IsMigrationTask = false; + IsRestorationTask = false; + CleanedUp = 0; + CleanupFails = 0; + foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + { + if (token.IsCancellationRequested) + return; + + try + { + File.Delete(file); + ++CleanedUp; + Penumbra.Log.Debug($"Deleted model backup file {file}."); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to delete model backup file {file}", NotificationType.Warning); + ++CleanupFails; + } + } + }, token); + } + + public void RestoreBackups(string path) + { + if (IsRunning) + return; + + _source = new CancellationTokenSource(); + var token = _source.Token; + _currentTask = Task.Run(() => + { + HasRestoreTask = true; + IsCleanupTask = false; + IsMigrationTask = false; + IsRestorationTask = true; + CleanedUp = 0; + CleanupFails = 0; + foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + { + if (token.IsCancellationRequested) + return; + + var target = file[..^4]; + try + { + File.Copy(file, target, true); + ++Restored; + Penumbra.Log.Debug($"Restored model backup file {file} to {target}."); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to restore model backup file {file} to {target}", + NotificationType.Warning); + ++RestoreFails; + } + } + }, token); + } + + /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. + public byte[] MigrateTtmpModel(string path, byte[] data) + { + FixLodNum(data); + if (!config.MigrateImportedModelsToV6) + return data; + + var version = BitConverter.ToUInt32(data); + if (version != 5) + return data; + + var mdl = new MdlFile(data); + if (!mdl.ConvertV5ToV6()) + return data; + + data = mdl.Write(); + Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import."); + return data; + } + + public void MigrateDirectory(string path, bool createBackups) + { + if (IsRunning) + return; + + _source = new CancellationTokenSource(); + var token = _source.Token; + _currentTask = Task.Run(() => + { + HasMigrationTask = true; + IsCleanupTask = false; + IsMigrationTask = true; + IsRestorationTask = false; + Unchanged = 0; + Migrated = 0; + Failed = 0; + foreach (var file in Directory.EnumerateFiles(path, "*.mdl", SearchOption.AllDirectories)) + { + if (token.IsCancellationRequested) + return; + + var timer = Stopwatch.StartNew(); + try + { + var data = File.ReadAllBytes(file); + var mdl = new MdlFile(data); + if (MigrateModel(file, mdl, createBackups)) + { + ++Migrated; + Penumbra.Log.Debug($"Migrated model file {file} from V5 to V6 in {timer.ElapsedMilliseconds} ms."); + } + else + { + ++Unchanged; + Penumbra.Log.Verbose($"Verified that model file {file} is already V6 in {timer.ElapsedMilliseconds} ms."); + } + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate model file {file} to V6 in {timer.ElapsedMilliseconds} ms", + NotificationType.Warning); + ++Failed; + } + } + }, token); + } + + public void Cancel() + { + _source?.Cancel(); + _source = null; + _currentTask = null; + } + + private static void FixLodNum(byte[] data) + { + const int modelHeaderLodOffset = 22; + + // Model file header LOD num + data[64] = 1; + + // Model header LOD num + var stackSize = BitConverter.ToUInt32(data, 4); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + data[modelHeaderStart + modelHeaderLodOffset] = 1; + } + + public static bool TryMigrateSingleModel(string path, bool createBackup) + { + try + { + var data = File.ReadAllBytes(path); + var mdl = new MdlFile(data); + return MigrateModel(path, mdl, createBackup); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate the model {path} to V6", NotificationType.Warning); + return false; + } + } + + public static bool TryMigrateSingleMaterial(string path, bool createBackup) + { + try + { + var data = File.ReadAllBytes(path); + var mtrl = new MtrlFile(data); + return MigrateMaterial(path, mtrl, createBackup); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate the material {path} to Dawntrail", NotificationType.Warning); + return false; + } + } + + private static bool MigrateModel(string path, MdlFile mdl, bool createBackup) + { + if (!mdl.ConvertV5ToV6()) + return false; + + var data = mdl.Write(); + if (createBackup) + File.Copy(path, Path.ChangeExtension(path, ".mdl.bak")); + File.WriteAllBytes(path, data); + return true; + } + + private static bool MigrateMaterial(string path, MtrlFile mtrl, bool createBackup) + { + if (!mtrl.MigrateToDawntrail()) + return false; + + var data = mtrl.Write(); + + mtrl.Write(); + if (createBackup) + File.Copy(path, Path.ChangeExtension(path, ".mtrl.bak")); + File.WriteAllBytes(path, data); + return true; + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 68b3717f..0223ca6b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.String.Classes; @@ -16,6 +17,7 @@ public partial class ModEditWindow private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { + DrawVersionUpdate(tab, disabled); DrawMaterialLivePreviewRebind(tab, disabled); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); @@ -34,6 +36,20 @@ public partial class ModEditWindow return !disabled && ret; } + private void DrawVersionUpdate(MtrlTab tab, bool disabled) + { + if (disabled || tab.Mtrl.IsDawnTrail) + return; + + if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, + "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return; + + tab.Mtrl.MigrateToDawntrail(); + _materialTab.SaveFile(); + } + private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) { if (disabled) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 90fdc48e..83a8958b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -37,6 +37,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; + public readonly MigrationManager MigrationManager; + private readonly PerformanceTracker _performance; private readonly ModEditor _editor; private readonly Configuration _config; @@ -588,7 +590,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers) + CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager) : base(WindowBaseLabel) { _performance = performance; @@ -608,6 +610,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; + MigrationManager = migrationManager; _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 6c8bbf64..0f9b2518 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -37,9 +37,9 @@ public class CollectionSelectHeader : IUiService var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); using (var _ = ImRaii.Group()) { - DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); + DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2); - DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); + DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4); _collectionCombo.Draw("##collectionSelector", comboWidth, ColorId.SelectedCollection.Value()); diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs new file mode 100644 index 00000000..75d37368 --- /dev/null +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -0,0 +1,121 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Services; + +namespace Penumbra.UI.Classes; + +public class MigrationSectionDrawer(MigrationManager migrationManager, Configuration config) : IUiService +{ + private bool _createBackups = true; + private Vector2 _buttonSize; + + public void Draw() + { + using var header = ImUtf8.CollapsingHeaderId("Migration"u8); + if (!header) + return; + + _buttonSize = UiHelpers.InputTextWidth; + DrawSettings(); + ImGui.Separator(); + DrawMigration(); + ImGui.Separator(); + DrawCleanup(); + ImGui.Separator(); + DrawRestore(); + } + + private void DrawSettings() + { + var value = config.MigrateImportedModelsToV6; + if (ImUtf8.Checkbox("Automatically Migrate V5 Models to V6 on Import"u8, ref value)) + { + config.MigrateImportedModelsToV6 = value; + config.Save(); + } + } + + private void DrawMigration() + { + ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); + if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.MigrateDirectory(config.ModDirectory, _createBackups); + + ImUtf8.SameLineInner(); + DrawCancelButton(0, "Cancel the migration. This does not revert already finished migrations."u8); + DrawSpinner(migrationManager is { IsMigrationTask: true, IsRunning: true }); + + if (!migrationManager.HasMigrationTask) + { + ImUtf8.IconDummy(); + return; + } + + var total = migrationManager.Failed + migrationManager.Migrated + migrationManager.Unchanged; + if (total == 0) + ImUtf8.TextFrameAligned("No model files found."u8); + else + ImUtf8.TextFrameAligned($"{migrationManager.Migrated} files migrated, {migrationManager.Failed} files failed, {total} total files."); + } + + private void DrawCleanup() + { + if (ImUtf8.ButtonEx("Delete Existing Model Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.CleanBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(1, "Cancel the cleanup. This is not revertible."u8); + DrawSpinner(migrationManager is { IsCleanupTask: true, IsRunning: true }); + if (!migrationManager.HasCleanUpTask) + { + ImUtf8.IconDummy(); + return; + } + + var total = migrationManager.CleanedUp + migrationManager.CleanupFails; + if (total == 0) + ImUtf8.TextFrameAligned("No model backup files found."u8); + else + ImUtf8.TextFrameAligned( + $"{migrationManager.CleanedUp} backups deleted, {migrationManager.CleanupFails} deletions failed, {total} total backups."); + } + + private void DrawSpinner(bool enabled) + { + if (!enabled) + return; + ImGui.SameLine(); + ImUtf8.Spinner("Spinner"u8, ImGui.GetTextLineHeight() / 2, 2, ImGui.GetColorU32(ImGuiCol.Text)); + } + + private void DrawRestore() + { + if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.RestoreBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(2, "Cancel the restoration. This does not revert already finished restoration."u8); + DrawSpinner(migrationManager is { IsRestorationTask: true, IsRunning: true }); + + if (!migrationManager.HasRestoreTask) + { + ImUtf8.IconDummy(); + return; + } + + var total = migrationManager.Restored + migrationManager.RestoreFails; + if (total == 0) + ImUtf8.TextFrameAligned("No model backup files found."u8); + else + ImUtf8.TextFrameAligned( + $"{migrationManager.Restored} backups restored, {migrationManager.RestoreFails} restorations failed, {total} total backups."); + } + + private void DrawCancelButton(int id, ReadOnlySpan tooltip) + { + using var _ = ImUtf8.PushId(id); + if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning)) + migrationManager.Cancel(); + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 49e77a4d..8a4d6874 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -41,10 +41,11 @@ public class SettingsTab : ITab, IUiService private readonly DalamudSubstitutionProvider _dalamudSubstitutionProvider; private readonly FileCompactor _compactor; private readonly DalamudConfigService _dalamudConfig; - private readonly IDalamudPluginInterface _pluginInterface; + private readonly IDalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; private readonly PredefinedTagManager _predefinedTagManager; private readonly CrashHandlerService _crashService; + private readonly MigrationSectionDrawer _migrationDrawer; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -55,7 +56,8 @@ public class SettingsTab : ITab, IUiService Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService) + IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, + MigrationSectionDrawer migrationDrawer) { _pluginInterface = pluginInterface; _config = config; @@ -77,6 +79,7 @@ public class SettingsTab : ITab, IUiService _compactor.Enabled = _config.UseFileSystemCompression; _predefinedTagManager = predefinedTagConfig; _crashService = crashService; + _migrationDrawer = migrationDrawer; } public void DrawHeader() @@ -102,6 +105,7 @@ public class SettingsTab : ITab, IUiService ImGui.NewLine(); DrawGeneralSettings(); + _migrationDrawer.Draw(); DrawColorSettings(); DrawPredefinedTagsSection(); DrawAdvancedSettings(); From f89eea721f524a4acc9b3e8041871bbbd1d8db66 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jul 2024 14:59:35 +0200 Subject: [PATCH 208/865] Update game data. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b5eb074d..d7a56b70 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b5eb074d80a4aaa2703a3124d2973a7fa08046ad +Subproject commit d7a56b708c73bc9917baeaa66842c1594ca3067b From 710f39768bec527db1de4172499dee2b80c7ac24 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jul 2024 15:00:37 +0200 Subject: [PATCH 209/865] Disable the required ShadersKnown for the time being. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 91129129..cd7aca9d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -691,8 +691,9 @@ public partial class ModEditWindow _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); } + // TODO Readd ShadersKnown public bool Valid - => ShadersKnown && Mtrl.Valid; + => (true || ShadersKnown) && Mtrl.Valid; public byte[] Write() { From b677a14ceffe6d8a1c0e94d47e1af3bfd19e1616 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 16:34:31 +0200 Subject: [PATCH 210/865] Update. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Configuration.cs | 3 +- Penumbra/Import/TexToolsImporter.Archives.cs | 11 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 5 +- .../Animation/ApricotListenerSoundPlay.cs | 2 + Penumbra/Services/MigrationManager.cs | 340 +++++++++++------- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 155 +++++--- Penumbra/UI/Tabs/Debug/DebugTab.cs | 10 +- 9 files changed, 338 insertions(+), 192 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 43b0b47f..f4c6144c 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 43b0b47f2d019af0fe4681dfc578f9232e3ba90c +Subproject commit f4c6144ca2012b279e6d8aa52b2bef6cc2ba32d9 diff --git a/Penumbra.GameData b/Penumbra.GameData index d7a56b70..cf5be8af 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d7a56b708c73bc9917baeaa66842c1594ca3067b +Subproject commit cf5be8af4c9ecbd9190bd3db746743fa5cd1560f diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8d0f7fd8..f16569b5 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -99,7 +99,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; - public bool MigrateImportedModelsToV6 { get; set; } = false; + public bool MigrateImportedModelsToV6 { get; set; } = true; + public bool MigrateImportedMaterialsToLegacy { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; public bool AlwaysOpenDefaultImport { get; set; } = false; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index a51dbc61..63c170cb 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -1,3 +1,4 @@ +using System.IO; using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -90,7 +91,7 @@ public partial class TexToolsImporter } else { - HandleFileMigrations(reader); + HandleFileMigrationsAndWrite(reader); } ++_currentFileIdx; @@ -118,13 +119,19 @@ public partial class TexToolsImporter } - private void HandleFileMigrations(IReader reader) + private void HandleFileMigrationsAndWrite(IReader reader) { switch (Path.GetExtension(reader.Entry.Key)) { case ".mdl": _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); break; + case ".mtrl": + _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); + break; + default: + reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); + break; } } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index ba294353..3ae1eda9 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -255,8 +255,9 @@ public partial class TexToolsImporter data.Data = Path.GetExtension(extractedFile.FullName) switch { - ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), - _ => data.Data, + ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), + ".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data), + _ => data.Data, }; _compactor.WriteAllBytesAsync(extractedFile.FullName, data.Data, _token).Wait(_token); diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index e58c7268..2e05c1b6 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -1,3 +1,4 @@ +using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; @@ -16,6 +17,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook Changed + Unchanged + Failed; + + public void Init() + { + Changed = 0; + Unchanged = 0; + Failed = 0; + HasData = true; + } + } + private Task? _currentTask; private CancellationTokenSource? _source; - public bool HasCleanUpTask { get; private set; } - public bool HasMigrationTask { get; private set; } - public bool HasRestoreTask { get; private set; } + public TaskType CurrentTask { get; private set; } - public bool IsMigrationTask { get; private set; } - public bool IsRestorationTask { get; private set; } - public bool IsCleanupTask { get; private set; } + public readonly MigrationData MdlMigration = new(true); + public readonly MigrationData MtrlMigration = new(true); + public readonly MigrationData MdlCleanup = new(false); + public readonly MigrationData MtrlCleanup = new(false); + public readonly MigrationData MdlRestoration = new(false); + public readonly MigrationData MtrlRestoration = new(false); - public int Restored { get; private set; } - public int RestoreFails { get; private set; } - - public int CleanedUp { get; private set; } - - public int CleanupFails { get; private set; } - - public int Migrated { get; private set; } - - public int Unchanged { get; private set; } - - public int Failed { get; private set; } - public bool IsRunning => _currentTask is { IsCompleted: false }; - /// Writes or migrates a .mdl file during extraction from a regular archive. - public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) - { - if (!config.MigrateImportedModelsToV6) - { - reader.WriteEntryToDirectory(directory, options); - return; - } + public void CleanMdlBackups(string path) + => CleanBackups(path, "*.mdl.bak", "model", MdlCleanup, TaskType.MdlCleanup); - var path = Path.Combine(directory, reader.Entry.Key); - using var s = new MemoryStream(); - using var e = reader.OpenEntryStream(); - e.CopyTo(s); - using var b = new BinaryReader(s); - var version = b.ReadUInt32(); - if (version == MdlFile.V5) - { - var data = s.ToArray(); - var mdl = new MdlFile(data); - MigrateModel(path, mdl, false); - Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); - } - else - { - using var f = File.Open(path, FileMode.Create, FileAccess.Write); - s.Seek(0, SeekOrigin.Begin); - s.WriteTo(f); - } - } + public void CleanMtrlBackups(string path) + => CleanBackups(path, "*.mtrl.bak", "material", MtrlCleanup, TaskType.MtrlCleanup); - public void CleanBackups(string path) + private void CleanBackups(string path, string extension, string fileType, MigrationData data, TaskType type) { if (IsRunning) return; @@ -76,13 +72,9 @@ public class MigrationManager(Configuration config) : IService var token = _source.Token; _currentTask = Task.Run(() => { - HasCleanUpTask = true; - IsCleanupTask = true; - IsMigrationTask = false; - IsRestorationTask = false; - CleanedUp = 0; - CleanupFails = 0; - foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + CurrentTask = type; + data.Init(); + foreach (var file in Directory.EnumerateFiles(path, extension, SearchOption.AllDirectories)) { if (token.IsCancellationRequested) return; @@ -90,19 +82,25 @@ public class MigrationManager(Configuration config) : IService try { File.Delete(file); - ++CleanedUp; - Penumbra.Log.Debug($"Deleted model backup file {file}."); + ++data.Changed; + Penumbra.Log.Debug($"Deleted {fileType} backup file {file}."); } catch (Exception ex) { - Penumbra.Messager.NotificationMessage(ex, $"Failed to delete model backup file {file}", NotificationType.Warning); - ++CleanupFails; + Penumbra.Messager.NotificationMessage(ex, $"Failed to delete {fileType} backup file {file}", NotificationType.Warning); + ++data.Failed; } } }, token); } - public void RestoreBackups(string path) + public void RestoreMdlBackups(string path) + => RestoreBackups(path, "*.mdl.bak", "model", MdlRestoration, TaskType.MdlRestoration); + + public void RestoreMtrlBackups(string path) + => RestoreBackups(path, "*.mtrl.bak", "material", MtrlRestoration, TaskType.MtrlRestoration); + + private void RestoreBackups(string path, string extension, string fileType, MigrationData data, TaskType type) { if (IsRunning) return; @@ -111,13 +109,9 @@ public class MigrationManager(Configuration config) : IService var token = _source.Token; _currentTask = Task.Run(() => { - HasRestoreTask = true; - IsCleanupTask = false; - IsMigrationTask = false; - IsRestorationTask = true; - CleanedUp = 0; - CleanupFails = 0; - foreach (var file in Directory.EnumerateFiles(path, "*.mdl.bak", SearchOption.AllDirectories)) + CurrentTask = type; + data.Init(); + foreach (var file in Directory.EnumerateFiles(path, extension, SearchOption.AllDirectories)) { if (token.IsCancellationRequested) return; @@ -126,40 +120,38 @@ public class MigrationManager(Configuration config) : IService try { File.Copy(file, target, true); - ++Restored; - Penumbra.Log.Debug($"Restored model backup file {file} to {target}."); + ++data.Changed; + Penumbra.Log.Debug($"Restored {fileType} backup file {file} to {target}."); } catch (Exception ex) { - Penumbra.Messager.NotificationMessage(ex, $"Failed to restore model backup file {file} to {target}", + Penumbra.Messager.NotificationMessage(ex, $"Failed to restore {fileType} backup file {file} to {target}", NotificationType.Warning); - ++RestoreFails; + ++data.Failed; } } }, token); } - /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. - public byte[] MigrateTtmpModel(string path, byte[] data) - { - FixLodNum(data); - if (!config.MigrateImportedModelsToV6) - return data; + public void MigrateMdlDirectory(string path, bool createBackups) + => MigrateDirectory(path, createBackups, "*.mdl", "model", MdlMigration, TaskType.MdlMigration, "from V5 to V6", "V6", + (file, fileData, backups) => + { + var mdl = new MdlFile(fileData); + return MigrateModel(file, mdl, backups); + }); - var version = BitConverter.ToUInt32(data); - if (version != 5) - return data; + public void MigrateMtrlDirectory(string path, bool createBackups) + => MigrateDirectory(path, createBackups, "*.mtrl", "material", MtrlMigration, TaskType.MtrlMigration, "to Dawntrail", "Dawntrail", + (file, fileData, backups) => + { + var mtrl = new MtrlFile(fileData); + return MigrateMaterial(file, mtrl, backups); + } + ); - var mdl = new MdlFile(data); - if (!mdl.ConvertV5ToV6()) - return data; - - data = mdl.Write(); - Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import."); - return data; - } - - public void MigrateDirectory(string path, bool createBackups) + private void MigrateDirectory(string path, bool createBackups, string extension, string fileType, MigrationData data, TaskType type, + string action, string state, Func func) { if (IsRunning) return; @@ -168,14 +160,9 @@ public class MigrationManager(Configuration config) : IService var token = _source.Token; _currentTask = Task.Run(() => { - HasMigrationTask = true; - IsCleanupTask = false; - IsMigrationTask = true; - IsRestorationTask = false; - Unchanged = 0; - Migrated = 0; - Failed = 0; - foreach (var file in Directory.EnumerateFiles(path, "*.mdl", SearchOption.AllDirectories)) + CurrentTask = type; + data.Init(); + foreach (var file in Directory.EnumerateFiles(path, extension, SearchOption.AllDirectories)) { if (token.IsCancellationRequested) return; @@ -183,24 +170,24 @@ public class MigrationManager(Configuration config) : IService var timer = Stopwatch.StartNew(); try { - var data = File.ReadAllBytes(file); - var mdl = new MdlFile(data); - if (MigrateModel(file, mdl, createBackups)) + var fileData = File.ReadAllBytes(file); + if (func(file, fileData, createBackups)) { - ++Migrated; - Penumbra.Log.Debug($"Migrated model file {file} from V5 to V6 in {timer.ElapsedMilliseconds} ms."); + ++data.Changed; + Penumbra.Log.Debug($"Migrated {fileType} file {file} {action} in {timer.ElapsedMilliseconds} ms."); } else { - ++Unchanged; - Penumbra.Log.Verbose($"Verified that model file {file} is already V6 in {timer.ElapsedMilliseconds} ms."); + ++data.Unchanged; + Penumbra.Log.Verbose($"Verified that {fileType} file {file} is already {state} in {timer.ElapsedMilliseconds} ms."); } } catch (Exception ex) { - Penumbra.Messager.NotificationMessage(ex, $"Failed to migrate model file {file} to V6 in {timer.ElapsedMilliseconds} ms", + ++data.Failed; + Penumbra.Messager.NotificationMessage(ex, + $"Failed to migrate {fileType} file {file} to {state} in {timer.ElapsedMilliseconds} ms", NotificationType.Warning); - ++Failed; } } }, token); @@ -213,22 +200,6 @@ public class MigrationManager(Configuration config) : IService _currentTask = null; } - private static void FixLodNum(byte[] data) - { - const int modelHeaderLodOffset = 22; - - // Model file header LOD num - data[64] = 1; - - // Model header LOD num - var stackSize = BitConverter.ToUInt32(data, 4); - var runtimeBegin = stackSize + 0x44; - var stringsLengthOffset = runtimeBegin + 4; - var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset); - var modelHeaderStart = stringsLengthOffset + stringsLength + 4; - data[modelHeaderStart + modelHeaderLodOffset] = 1; - } - public static bool TryMigrateSingleModel(string path, bool createBackup) { try @@ -259,6 +230,113 @@ public class MigrationManager(Configuration config) : IService } } + /// Writes or migrates a .mdl file during extraction from a regular archive. + public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) + { + if (!config.MigrateImportedModelsToV6) + { + reader.WriteEntryToDirectory(directory, options); + return; + } + + var path = Path.Combine(directory, reader.Entry.Key); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + using var b = new BinaryReader(s); + var version = b.ReadUInt32(); + if (version == MdlFile.V5) + { + var data = s.ToArray(); + var mdl = new MdlFile(data); + MigrateModel(path, mdl, false); + Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); + } + else + { + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } + } + + public void MigrateMtrlDuringExtraction(IReader reader, string directory, ExtractionOptions options) + { + if (!config.MigrateImportedMaterialsToLegacy) + { + reader.WriteEntryToDirectory(directory, options); + return; + } + + var path = Path.Combine(directory, reader.Entry.Key); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + var file = new MtrlFile(s.GetBuffer()); + if (!file.IsDawnTrail) + { + file.MigrateToDawntrail(); + Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); + } + + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } + + /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. + public byte[] MigrateTtmpModel(string path, byte[] data) + { + FixLodNum(data); + if (!config.MigrateImportedModelsToV6) + return data; + + var version = BitConverter.ToUInt32(data); + if (version != 5) + return data; + + try + { + var mdl = new MdlFile(data); + if (!mdl.ConvertV5ToV6()) + return data; + + data = mdl.Write(); + Penumbra.Log.Debug($"Migrated model {path} from V5 to V6 during import."); + return data; + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Failed to migrate model {path} from V5 to V6 during import:\n{ex}"); + return data; + } + } + + /// Update the data of a .mtrl file during TTMP extraction. Returns either the existing array or a new one. + public byte[] MigrateTtmpMaterial(string path, byte[] data) + { + if (!config.MigrateImportedMaterialsToLegacy) + return data; + + try + { + var mtrl = new MtrlFile(data); + if (mtrl.IsDawnTrail) + return data; + + mtrl.MigrateToDawntrail(); + data = mtrl.Write(); + Penumbra.Log.Debug($"Migrated material {path} to Dawntrail during import."); + return data; + } + catch (Exception ex) + { + Penumbra.Log.Warning($"Failed to migrate material {path} to Dawntrail during import:\n{ex}"); + return data; + } + } + + private static bool MigrateModel(string path, MdlFile mdl, bool createBackup) { if (!mdl.ConvertV5ToV6()) @@ -284,4 +362,20 @@ public class MigrationManager(Configuration config) : IService File.WriteAllBytes(path, data); return true; } + + private static void FixLodNum(byte[] data) + { + const int modelHeaderLodOffset = 22; + + // Model file header LOD num + data[64] = 1; + + // Model header LOD num + var stackSize = BitConverter.ToUInt32(data, 4); + var runtimeBegin = stackSize + 0x44; + var stringsLengthOffset = runtimeBegin + 4; + var stringsLength = BitConverter.ToUInt32(data, (int)stringsLengthOffset); + var modelHeaderStart = stringsLengthOffset + stringsLength + 4; + data[modelHeaderStart + modelHeaderLodOffset] = 1; + } } diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index 75d37368..ec76ddae 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -19,11 +19,13 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura _buttonSize = UiHelpers.InputTextWidth; DrawSettings(); ImGui.Separator(); - DrawMigration(); + DrawMdlMigration(); + DrawMdlRestore(); + DrawMdlCleanup(); ImGui.Separator(); - DrawCleanup(); - ImGui.Separator(); - DrawRestore(); + DrawMtrlMigration(); + DrawMtrlRestore(); + DrawMtrlCleanup(); } private void DrawSettings() @@ -34,88 +36,125 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura config.MigrateImportedModelsToV6 = value; config.Save(); } - } - private void DrawMigration() - { - ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); - if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) - migrationManager.MigrateDirectory(config.ModDirectory, _createBackups); + ImUtf8.HoverTooltip("This increments the version marker and restructures the bone table to the new version."u8); - ImUtf8.SameLineInner(); - DrawCancelButton(0, "Cancel the migration. This does not revert already finished migrations."u8); - DrawSpinner(migrationManager is { IsMigrationTask: true, IsRunning: true }); - - if (!migrationManager.HasMigrationTask) + if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) { - ImUtf8.IconDummy(); - return; + config.MigrateImportedMaterialsToLegacy = value; + config.Save(); } - var total = migrationManager.Failed + migrationManager.Migrated + migrationManager.Unchanged; - if (total == 0) - ImUtf8.TextFrameAligned("No model files found."u8); - else - ImUtf8.TextFrameAligned($"{migrationManager.Migrated} files migrated, {migrationManager.Failed} files failed, {total} total files."); + ImUtf8.HoverTooltip( + "This currently only increases the color-table size and switches the shader from 'character.shpk' to 'characterlegacy.shpk', if the former is used."u8); + + ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); } - private void DrawCleanup() + private static ReadOnlySpan MigrationTooltip + => "Cancel the migration. This does not revert already finished migrations."u8; + + private void DrawMdlMigration() + { + if (ImUtf8.ButtonEx("Migrate Model Files From V5 to V6"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.MigrateMdlDirectory(config.ModDirectory, _createBackups); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MdlMigration, "Cancel the migration. This does not revert already finished migrations."u8); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MdlMigration, IsRunning: true }); + DrawData(migrationManager.MdlMigration, "No model files found."u8, "migrated"u8); + } + + private void DrawMtrlMigration() + { + if (ImUtf8.ButtonEx("Migrate Material Files to Dawntrail"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.MigrateMtrlDirectory(config.ModDirectory, _createBackups); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MtrlMigration, MigrationTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MtrlMigration, IsRunning: true }); + DrawData(migrationManager.MtrlMigration, "No material files found."u8, "migrated"u8); + } + + + private static ReadOnlySpan CleanupTooltip + => "Cancel the cleanup. This is not revertible."u8; + + private void DrawMdlCleanup() { if (ImUtf8.ButtonEx("Delete Existing Model Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) - migrationManager.CleanBackups(config.ModDirectory); + migrationManager.CleanMdlBackups(config.ModDirectory); ImUtf8.SameLineInner(); - DrawCancelButton(1, "Cancel the cleanup. This is not revertible."u8); - DrawSpinner(migrationManager is { IsCleanupTask: true, IsRunning: true }); - if (!migrationManager.HasCleanUpTask) - { - ImUtf8.IconDummy(); - return; - } - - var total = migrationManager.CleanedUp + migrationManager.CleanupFails; - if (total == 0) - ImUtf8.TextFrameAligned("No model backup files found."u8); - else - ImUtf8.TextFrameAligned( - $"{migrationManager.CleanedUp} backups deleted, {migrationManager.CleanupFails} deletions failed, {total} total backups."); + DrawCancelButton(MigrationManager.TaskType.MdlCleanup, CleanupTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MdlCleanup, IsRunning: true }); + DrawData(migrationManager.MdlCleanup, "No model backup files found."u8, "deleted"u8); } - private void DrawSpinner(bool enabled) + private void DrawMtrlCleanup() + { + if (ImUtf8.ButtonEx("Delete Existing Material Backup Files"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.CleanMtrlBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MtrlCleanup, CleanupTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MtrlCleanup, IsRunning: true }); + DrawData(migrationManager.MtrlCleanup, "No material backup files found."u8, "deleted"u8); + } + + private static ReadOnlySpan RestorationTooltip + => "Cancel the restoration. This does not revert already finished restoration."u8; + + private void DrawMdlRestore() + { + if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.RestoreMdlBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MdlRestoration, RestorationTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MdlRestoration, IsRunning: true }); + DrawData(migrationManager.MdlRestoration, "No model backup files found."u8, "restored"u8); + } + + private void DrawMtrlRestore() + { + if (ImUtf8.ButtonEx("Restore Material Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) + migrationManager.RestoreMtrlBackups(config.ModDirectory); + + ImUtf8.SameLineInner(); + DrawCancelButton(MigrationManager.TaskType.MtrlRestoration, RestorationTooltip); + DrawSpinner(migrationManager is { CurrentTask: MigrationManager.TaskType.MtrlRestoration, IsRunning: true }); + DrawData(migrationManager.MtrlRestoration, "No material backup files found."u8, "restored"u8); + } + + private static void DrawSpinner(bool enabled) { if (!enabled) return; + ImGui.SameLine(); ImUtf8.Spinner("Spinner"u8, ImGui.GetTextLineHeight() / 2, 2, ImGui.GetColorU32(ImGuiCol.Text)); } - private void DrawRestore() + private void DrawCancelButton(MigrationManager.TaskType task, ReadOnlySpan tooltip) { - if (ImUtf8.ButtonEx("Restore Model Backups"u8, "\0"u8, _buttonSize, migrationManager.IsRunning)) - migrationManager.RestoreBackups(config.ModDirectory); + using var _ = ImUtf8.PushId((int)task); + if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning || task != migrationManager.CurrentTask)) + migrationManager.Cancel(); + } - ImUtf8.SameLineInner(); - DrawCancelButton(2, "Cancel the restoration. This does not revert already finished restoration."u8); - DrawSpinner(migrationManager is { IsRestorationTask: true, IsRunning: true }); - - if (!migrationManager.HasRestoreTask) + private static void DrawData(MigrationManager.MigrationData data, ReadOnlySpan empty, ReadOnlySpan action) + { + if (!data.HasData) { ImUtf8.IconDummy(); return; } - var total = migrationManager.Restored + migrationManager.RestoreFails; + var total = data.Total; if (total == 0) - ImUtf8.TextFrameAligned("No model backup files found."u8); + ImUtf8.TextFrameAligned(empty); else - ImUtf8.TextFrameAligned( - $"{migrationManager.Restored} backups restored, {migrationManager.RestoreFails} restorations failed, {total} total backups."); - } - - private void DrawCancelButton(int id, ReadOnlySpan tooltip) - { - using var _ = ImUtf8.PushId(id); - if (ImUtf8.ButtonEx("Cancel"u8, tooltip, disabled: !migrationManager.IsRunning)) - migrationManager.Cancel(); + ImUtf8.TextFrameAligned($"{data.Changed} files {action}, {data.Failed} files failed, {total} files found."); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ace3d6a3..be92b94e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -433,10 +434,11 @@ public class DebugTab : Window, ITab, IUiService foreach (var obj in _objects) { ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); - ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); - ImGuiUtil.DrawTableColumn(obj.Address == nint.Zero - ? string.Empty - : $"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable($"0x{obj.Address:X}"); + ImGui.TableNextColumn(); + if (obj.Address != nint.Zero) + ImGuiUtil.CopyOnClickSelectable($"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); var identifier = _actors.FromObject(obj, out _, false, true, false); ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc From a0a3435918c8f644aa4cedefcd3d9efe8643ccf4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 16:50:48 +0200 Subject: [PATCH 211/865] Remove not-yet-existing CS requirement. --- Penumbra.GameData | 2 +- repo.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index cf5be8af..d8c784e4 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit cf5be8af4c9ecbd9190bd3db746743fa5cd1560f +Subproject commit d8c784e443112d17d93ba7e6ab5d54f00f7f1477 diff --git a/repo.json b/repo.json index 3142f8d4..6379595e 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.1.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 56502f19f9aa41003d532d68f225f7c956fa0f22 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 14:55:15 +0000 Subject: [PATCH 212/865] [CI] Updating repo.json for testing_1.2.0.0 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6379595e..c604a0dd 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.1.1.5", + "TestingAssemblyVersion": "1.2.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.1.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c2517499f27f1a7c83ad9510d3cbf9ad6ad255e1 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:04:27 +0200 Subject: [PATCH 213/865] Update repo.json --- repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.json b/repo.json index c604a0dd..4f567253 100644 --- a/repo.json +++ b/repo.json @@ -11,7 +11,7 @@ "ApplicableVersion": "any", "DalamudApiLevel": 10, "IsHide": "False", - "IsTestingExclusive": "False", + "IsTestingExclusive": "True", "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, From 3c417d7aeca0fd5668475e7644185c73f31e50b3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 17:48:34 +0200 Subject: [PATCH 214/865] Fix extraction of pmp failing --- Penumbra/Services/MigrationManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 1e6cb6b6..e377b65e 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -249,11 +249,13 @@ public class MigrationManager(Configuration config) : IService { var data = s.ToArray(); var mdl = new MdlFile(data); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); MigrateModel(path, mdl, false); Penumbra.Log.Debug($"Migrated model {reader.Entry.Key} from V5 to V6 during import."); } else { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); s.Seek(0, SeekOrigin.Begin); s.WriteTo(f); @@ -279,6 +281,7 @@ public class MigrationManager(Configuration config) : IService Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); } + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); s.Seek(0, SeekOrigin.Begin); s.WriteTo(f); From 1efd4938343bb0106d4a2e7c7166c7eebf8c8f12 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 15:52:40 +0000 Subject: [PATCH 215/865] [CI] Updating repo.json for testing_1.2.0.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4f567253..4231cf65 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.0", + "TestingAssemblyVersion": "1.2.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 3b980c1a49e8292d5a602f8852c373345fe767e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 18:09:37 +0200 Subject: [PATCH 216/865] Fix two import bugs. --- Penumbra/Services/MigrationManager.cs | 1 + Penumbra/UI/Classes/MigrationSectionDrawer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index e377b65e..5b353912 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -243,6 +243,7 @@ public class MigrationManager(Configuration config) : IService using var s = new MemoryStream(); using var e = reader.OpenEntryStream(); e.CopyTo(s); + s.Position = 0; using var b = new BinaryReader(s); var version = b.ReadUInt32(); if (version == MdlFile.V5) diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index ec76ddae..d588eaa0 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -39,6 +39,7 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura ImUtf8.HoverTooltip("This increments the version marker and restructures the bone table to the new version."u8); + value = config.MigrateImportedMaterialsToLegacy; if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) { config.MigrateImportedMaterialsToLegacy = value; From 806e001bad44e7bb6748c6ff20856ff9dbb41633 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 16:11:41 +0000 Subject: [PATCH 217/865] [CI] Updating repo.json for testing_1.2.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4231cf65..a5ec39b5 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.1", + "TestingAssemblyVersion": "1.2.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From baa439d2463d3de0276005d70d9bc7ca954b9a26 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 18:31:24 +0200 Subject: [PATCH 218/865] Fix enable/disable draw offsets. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index d8c784e4..49c5ba01 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d8c784e443112d17d93ba7e6ab5d54f00f7f1477 +Subproject commit 49c5ba0115814f809aaf4f31e6dcf321efb52237 From 37ffe528699191c4a06d7fa2a37412b927d10f04 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 9 Jul 2024 18:36:45 +0200 Subject: [PATCH 219/865] Fix issue with file substitutions. --- Penumbra/Api/DalamudSubstitutionProvider.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 1c2cebcc..6347447a 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Collections; @@ -13,6 +14,7 @@ namespace Penumbra.Api; public class DalamudSubstitutionProvider : IDisposable, IApiService { private readonly ITextureSubstitutionProvider _substitution; + private readonly IUiBuilder _uiBuilder; private readonly ActiveCollectionData _activeCollectionData; private readonly Configuration _config; private readonly CommunicatorService _communicator; @@ -21,9 +23,10 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService => _config.UseDalamudUiTextureRedirection; public DalamudSubstitutionProvider(ITextureSubstitutionProvider substitution, ActiveCollectionData activeCollectionData, - Configuration config, CommunicatorService communicator) + Configuration config, CommunicatorService communicator, IUiBuilder ui) { _substitution = substitution; + _uiBuilder = ui; _activeCollectionData = activeCollectionData; _config = config; _communicator = communicator; @@ -41,6 +44,9 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService public void ResetSubstitutions(IEnumerable paths) { + if (!_uiBuilder.UiPrepared) + return; + var transformed = paths .Where(p => (p.Path.StartsWith("ui/"u8) || p.Path.StartsWith("common/font/"u8)) && p.Path.EndsWith(".tex"u8)) .Select(p => p.ToString()); @@ -91,10 +97,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService case ResolvedFileChanged.Type.Added: case ResolvedFileChanged.Type.Removed: case ResolvedFileChanged.Type.Replaced: - ResetSubstitutions(new[] - { - key, - }); + ResetSubstitutions([key]); break; case ResolvedFileChanged.Type.FullRecomputeStart: case ResolvedFileChanged.Type.FullRecomputeFinished: From e2112202a05e50fb5ec1ea7c5f21b0b6f43a08d2 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 9 Jul 2024 16:38:39 +0000 Subject: [PATCH 220/865] [CI] Updating repo.json for testing_1.2.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index a5ec39b5..758a10d0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.2", + "TestingAssemblyVersion": "1.2.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1be75444cd8344b8a5774193bdf097c9b9f47d17 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 10 Jul 2024 12:51:47 +0200 Subject: [PATCH 221/865] Update BNPC Names --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 49c5ba01..a64a30bf 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 49c5ba0115814f809aaf4f31e6dcf321efb52237 +Subproject commit a64a30bf29cf297285ecde0579830b4d7fbae2d9 From 380dd0cffb8bc675c04c282d8bf92f72c5a1c3bb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 11 Jul 2024 01:40:45 +0200 Subject: [PATCH 222/865] Fix texture writing. --- Penumbra/Import/Textures/TexFileParser.cs | 4 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 7 +- Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs | 117 ++++++++++++++++++++++ Penumbra/UI/Tabs/EffectiveTab.cs | 2 +- 4 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 09025b61..ae4a39c0 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -79,8 +79,8 @@ public static class TexFileParser w.Write(header.Width); w.Write(header.Height); w.Write(header.Depth); - w.Write(header.MipCount); - w.Write(header.MipUnknownFlag); // TODO Lumina Update + w.Write((byte)(header.MipCount | (header.MipUnknownFlag ? 0x80 : 0))); + w.Write(header.ArraySize); unsafe { w.Write(header.LodOffset[0]); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index be92b94e..4966dd64 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -15,7 +15,6 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Services; -using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -96,6 +95,7 @@ public class DebugTab : Window, ITab, IUiService private readonly IClientState _clientState; private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; + private readonly TexHeaderDrawer _texHeaderDrawer; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, @@ -105,7 +105,7 @@ public class DebugTab : Window, ITab, IUiService DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, - Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel) + Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -141,6 +141,7 @@ public class DebugTab : Window, ITab, IUiService _diagnostics = diagnostics; _ipcTester = ipcTester; _crashHandlerPanel = crashHandlerPanel; + _texHeaderDrawer = texHeaderDrawer; _objects = objects; _clientState = clientState; } @@ -176,6 +177,8 @@ public class DebugTab : Window, ITab, IUiService ImGui.NewLine(); DrawCollectionCaches(); ImGui.NewLine(); + _texHeaderDrawer.Draw(); + ImGui.NewLine(); DrawDebugCharacterUtility(); ImGui.NewLine(); DrawShaderReplacementFixer(); diff --git a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs new file mode 100644 index 00000000..08d51184 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs @@ -0,0 +1,117 @@ +using Dalamud.Interface.DragDrop; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using Lumina.Data.Files; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.Tabs.Debug; + +public class TexHeaderDrawer(IDragDropManager dragDrop) : IUiService +{ + private string? _path; + private TexFile.TexHeader _header; + private byte[]? _tex; + private Exception? _exception; + + public void Draw() + { + using var header = ImUtf8.CollapsingHeaderId("Tex Header"u8); + if (!header) + return; + + DrawDragDrop(); + DrawData(); + } + + private void DrawDragDrop() + { + dragDrop.CreateImGuiSource("TexFileDragDrop", m => m.Files.Count == 1 && m.Extensions.Contains(".tex"), m => + { + ImUtf8.Text($"Dragging {m.Files[0]}..."); + return true; + }); + + ImUtf8.Button("Drag .tex here..."); + if (dragDrop.CreateImGuiTarget("TexFileDragDrop", out var files, out _)) + ReadTex(files[0]); + } + + private void DrawData() + { + if (_path == null) + return; + + ImUtf8.TextFramed(_path, 0, borderColor: 0xFFFFFFFF); + + + if (_exception != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImUtf8.TextWrapped($"Failure to load file:\n{_exception}"); + } + else if (_tex != null) + { + using var table = ImRaii.Table("table", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + TableLine("Format"u8, _header.Format); + TableLine("Width"u8, _header.Width); + TableLine("Height"u8, _header.Height); + TableLine("Depth"u8, _header.Depth); + TableLine("Mip Levels"u8, _header.MipCount); + TableLine("Array Size"u8, _header.ArraySize); + TableLine("Type"u8, _header.Type); + TableLine("Mip Flag"u8, _header.MipUnknownFlag); + TableLine("Byte Size"u8, _tex.Length); + unsafe + { + TableLine("LoD Offset 0"u8, _header.LodOffset[0]); + TableLine("LoD Offset 1"u8, _header.LodOffset[1]); + TableLine("LoD Offset 2"u8, _header.LodOffset[2]); + TableLine("LoD Offset 0"u8, _header.OffsetToSurface[0]); + TableLine("LoD Offset 1"u8, _header.OffsetToSurface[1]); + TableLine("LoD Offset 2"u8, _header.OffsetToSurface[2]); + TableLine("LoD Offset 3"u8, _header.OffsetToSurface[3]); + TableLine("LoD Offset 4"u8, _header.OffsetToSurface[4]); + TableLine("LoD Offset 5"u8, _header.OffsetToSurface[5]); + TableLine("LoD Offset 6"u8, _header.OffsetToSurface[6]); + TableLine("LoD Offset 7"u8, _header.OffsetToSurface[7]); + TableLine("LoD Offset 8"u8, _header.OffsetToSurface[8]); + TableLine("LoD Offset 9"u8, _header.OffsetToSurface[9]); + TableLine("LoD Offset 10"u8, _header.OffsetToSurface[10]); + TableLine("LoD Offset 11"u8, _header.OffsetToSurface[11]); + TableLine("LoD Offset 12"u8, _header.OffsetToSurface[12]); + } + } + } + + private static void TableLine(ReadOnlySpan text, T value) + { + ImGui.TableNextColumn(); + ImUtf8.Text(text); + ImGui.TableNextColumn(); + ImUtf8.Text($"{value}"); + } + + private unsafe void ReadTex(string path) + { + try + { + _path = path; + _tex = File.ReadAllBytes(_path); + if (_tex.Length < sizeof(TexFile.TexHeader)) + throw new Exception($"Size {_tex.Length} does not include a header."); + + _header = MemoryMarshal.Read(_tex); + _exception = null; + } + catch (Exception ex) + { + _tex = null; + _exception = ex; + } + } +} diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 1b9af75c..e0cab43f 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -26,7 +26,7 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH SetupEffectiveSizes(); collectionHeader.Draw(true); DrawFilters(); - using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); + using var child = ImRaii.Child("##EffectiveChangesTab", ImGui.GetContentRegionAvail(), false); if (!child) return; From 34cbf37c32a24c9af7f1807653220d9ed0c47974 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 10 Jul 2024 23:43:04 +0000 Subject: [PATCH 223/865] [CI] Updating repo.json for testing_1.2.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 758a10d0..7e90f7a9 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.3", + "TestingAssemblyVersion": "1.2.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 24597d7dc0cfa14bf25928f47b8e1f50665cbeee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 16:20:17 +0200 Subject: [PATCH 224/865] Fix mod normalization skipping the default submod. --- Penumbra/Mods/Editor/ModNormalizer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index c0876f5d..30bf3d3f 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -277,6 +277,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer private void ApplyRedirections() { + _modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) _modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); From 40be298d67d7a823ea67f62848028843d1b72244 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 17:37:19 +0200 Subject: [PATCH 225/865] Add automatic reduplication for ui files in pmps, test. --- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImporter.Archives.cs | 4 +- Penumbra/Import/TexToolsImporter.Gui.cs | 66 +++++------ Penumbra/Mods/Editor/ModNormalizer.cs | 109 ++++++++++++++++++- Penumbra/Penumbra.cs | 1 + Penumbra/UI/Tabs/SettingsTab.cs | 3 + 6 files changed, 145 insertions(+), 39 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index f16569b5..63325433 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -96,6 +96,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool AutoDeduplicateOnImport { get; set; } = true; + public bool AutoReduplicateUiOnImport { get; set; } = true; public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 63c170cb..dea343c6 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -1,9 +1,7 @@ -using System.IO; using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; -using Penumbra.GameData.Files; using Penumbra.Import.Structs; using Penumbra.Mods; using SharpCompress.Archives; @@ -11,7 +9,6 @@ using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; using SharpCompress.Readers; -using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; namespace Penumbra.Import; @@ -114,6 +111,7 @@ public partial class TexToolsImporter _currentModDirectory.Refresh(); _modManager.Creator.SplitMultiGroups(_currentModDirectory); + _editor.ModNormalizer.NormalizeUi(_currentModDirectory); return _currentModDirectory; } diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 78665f30..309f107a 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -20,59 +20,61 @@ public partial class TexToolsImporter private string _currentOptionName = string.Empty; private string _currentFileName = string.Empty; - public void DrawProgressInfo(Vector2 size) + public bool DrawProgressInfo(Vector2 size) { if (_modPackCount == 0) { ImGuiUtil.Center("Nothing to extract."); + return false; } - else if (_modPackCount == _currentModPackIdx) - { - DrawEndState(); - } + + if (_modPackCount == _currentModPackIdx) + return DrawEndState(); + + ImGui.NewLine(); + var percentage = (float)_currentModPackIdx / _modPackCount; + ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); + ImGui.NewLine(); + if (State == ImporterState.DeduplicatingFiles) + ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); else + ImGui.TextUnformatted($"Extracting {_currentModName}..."); + + if (_currentNumOptions > 1) { ImGui.NewLine(); - var percentage = (float)_currentModPackIdx / _modPackCount; - ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); ImGui.NewLine(); - if (State == ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); - else - ImGui.TextUnformatted($"Extracting {_currentModName}..."); - - if (_currentNumOptions > 1) - { - ImGui.NewLine(); - ImGui.NewLine(); - percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; - ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); - ImGui.NewLine(); - if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted( - $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); - } - - ImGui.NewLine(); - ImGui.NewLine(); - percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; - ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; + ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + ImGui.TextUnformatted( + $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); } + + ImGui.NewLine(); + ImGui.NewLine(); + percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; + ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + ImGui.NewLine(); + if (State != ImporterState.DeduplicatingFiles) + ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + return false; } - private void DrawEndState() + private bool DrawEndState() { var success = ExtractedMods.Count(t => t.Error == null); + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + return true; + ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files."); ImGui.NewLine(); using var table = ImRaii.Table("##files", 2); if (!table) - return; + return false; foreach (var (file, dir, ex) in ExtractedMods) { @@ -91,6 +93,8 @@ public partial class TexToolsImporter ImGuiUtil.HoverTooltip(ex.ToString()); } } + + return false; } public bool DrawCancelButton(Vector2 size) diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 30bf3d3f..43cfc1ee 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -3,13 +3,15 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Services; using OtterGui.Tasks; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModNormalizer(ModManager _modManager, Configuration _config) : IService +public class ModNormalizer(ModManager modManager, Configuration config, SaveService saveService) : IService { private readonly List>> _redirections = []; @@ -39,6 +41,103 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer Worker = TrackedTask.Run(NormalizeSync); } + public void NormalizeUi(DirectoryInfo modDirectory) + { + if (!config.AutoReduplicateUiOnImport) + return; + + if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod) + return; + + Dictionary> paths = []; + Dictionary containers = []; + foreach (var container in mod.AllDataContainers) + { + foreach (var (gamePath, path) in container.Files) + { + if (!gamePath.Path.StartsWith("ui/"u8)) + continue; + + if (!paths.TryGetValue(path, out var list)) + { + list = []; + paths.Add(path, list); + } + + list.Add((container, gamePath)); + containers.TryAdd(container, string.Empty); + } + } + + foreach (var container in containers.Keys.ToList()) + { + if (container.Group == null) + containers[container] = mod.ModPath.FullName; + else + { + var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport); + var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport); + containers[container] = optionDir.FullName; + } + } + + var anyChanges = 0; + var modRootLength = mod.ModPath.FullName.Length + 1; + foreach (var (file, gamePaths) in paths) + { + if (gamePaths.Count < 2) + continue; + + var keptPath = false; + foreach (var (container, gamePath) in gamePaths) + { + var directory = containers[container]; + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFilePath = Path.Combine(directory, relPath); + if (newFilePath == file.FullName) + { + Penumbra.Log.Verbose($"[UIReduplication] Kept {file.FullName[modRootLength..]} because new path was identical."); + keptPath = true; + continue; + } + + try + { + Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)!); + File.Copy(file.FullName, newFilePath, false); + Penumbra.Log.Verbose($"[UIReduplication] Copied {file.FullName[modRootLength..]} to {newFilePath[modRootLength..]}."); + container.Files[gamePath] = new FullPath(newFilePath); + ++anyChanges; + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"[UIReduplication] Failed to copy {file.FullName[modRootLength..]} to {newFilePath[modRootLength..]}:\n{ex}"); + } + } + + if (keptPath) + continue; + + try + { + File.Delete(file.FullName); + Penumbra.Log.Verbose($"[UIReduplication] Deleted {file.FullName[modRootLength..]} because no new path matched."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[UIReduplication] Failed to delete {file.FullName[modRootLength..]}:\n{ex}"); + } + } + + if (anyChanges == 0) + return; + + saveService.Save(SaveType.ImmediateSync, new ModSaveGroup(mod.Default, config.ReplaceNonAsciiOnImport)); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); + Penumbra.Log.Information($"[UIReduplication] Saved groups after {anyChanges} changes."); + } + private void NormalizeSync() { try @@ -168,7 +267,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer // Normalize all other options. foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { - var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); + var groupDir = ModCreator.CreateModFolder(directory, group.Name, config.ReplaceNonAsciiOnImport, true); _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count); for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i) _redirections[groupIdx + 1].Add([]); @@ -188,7 +287,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { var name = option.GetName(); - var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true); + var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true); newDict.Clear(); newDict.EnsureCapacity(option.Files.Count); @@ -277,10 +376,10 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer private void ApplyRedirections() { - _modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); + modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) - _modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); + modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); ++Step; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 9f2db2e6..5f8d6805 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); + sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append( $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 8a4d6874..ab47ce7c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -757,6 +757,9 @@ public class SettingsTab : ITab, IUiService Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); + Checkbox("Auto Reduplicate UI Files on PMP Import", + "Automatically reduplicate and normalize UI-specific files on import from PMP files. This is STRONGLY recommended because deduplicated UI files crash the game.", + _config.AutoReduplicateUiOnImport, v => _config.AutoReduplicateUiOnImport = v); DrawCompressionBox(); Checkbox("Keep Default Metadata Changes on Import", "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " From 22af545e8db41e6ec712eb3294dfd54dec6b42e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 17:37:55 +0200 Subject: [PATCH 226/865] Add image strings to groups and mods to keep them in the json on saves. --- Penumbra/Mods/Groups/IModGroup.cs | 1 + Penumbra/Mods/Groups/ImcModGroup.cs | 2 ++ Penumbra/Mods/Groups/ModSaveGroup.cs | 2 ++ Penumbra/Mods/Groups/MultiModGroup.cs | 2 ++ Penumbra/Mods/Groups/SingleModGroup.cs | 2 ++ Penumbra/Mods/Manager/ModDataEditor.cs | 8 ++++++++ Penumbra/Mods/Mod.cs | 1 + Penumbra/Mods/ModMeta.cs | 1 + 8 files changed, 19 insertions(+) diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 00f47e25..9327ced9 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -27,6 +27,7 @@ public interface IModGroup public Mod Mod { get; } public string Name { get; set; } public string Description { get; set; } + public string Image { get; set; } public GroupType Type { get; } public GroupDrawBehaviour Behaviour { get; } public ModPriority Priority { get; set; } diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 46204d6c..03896134 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -20,6 +20,7 @@ public class ImcModGroup(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.Imc; @@ -170,6 +171,7 @@ public class ImcModGroup(Mod mod) : IModGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Image = json[nameof(Image)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index c82c67c7..c465822b 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -88,6 +88,8 @@ public readonly struct ModSaveGroup : ISavable jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Image)); + jWriter.WriteValue(group.Image); jWriter.WritePropertyName(nameof(group.Priority)); jWriter.WriteValue(group.Priority.Value); jWriter.WritePropertyName(nameof(group.Type)); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 95f49230..ee27d534 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -26,6 +26,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; @@ -69,6 +70,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Image = json[nameof(Image)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index a559d609..cc606f42 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -24,6 +24,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup 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 ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } @@ -65,6 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Image = json[nameof(Image)]?.ToObject() ?? string.Empty, Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 4ab9deb1..91ae4a4c 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -22,6 +22,7 @@ public enum ModDataChangeType : ushort Favorite = 0x0200, LocalTags = 0x0400, Note = 0x0800, + Image = 0x1000, } public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService @@ -113,6 +114,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; + var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; @@ -138,6 +140,12 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = newDescription; } + if (mod.Image != newImage) + { + changes |= ModDataChangeType.Image; + mod.Image = newImage; + } + if (mod.Version != newVersion) { changes |= ModDataChangeType.Version; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index a7f87dcd..fcea7133 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -51,6 +51,7 @@ public sealed class Mod : IMod public string Description { get; internal set; } = string.Empty; public string Version { get; internal set; } = string.Empty; public string Website { get; internal set; } = string.Empty; + public string Image { get; internal set; } = string.Empty; public IReadOnlyList ModTags { get; internal set; } = []; diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 870d6d4f..39dd20e4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -19,6 +19,7 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.Name), JToken.FromObject(mod.Name) }, { nameof(Mod.Author), JToken.FromObject(mod.Author) }, { nameof(Mod.Description), JToken.FromObject(mod.Description) }, + { nameof(Mod.Image), JToken.FromObject(mod.Image) }, { nameof(Mod.Version), JToken.FromObject(mod.Version) }, { nameof(Mod.Website), JToken.FromObject(mod.Website) }, { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, From 94a05afbe0ce00f2c1195ca8ac206bc6f29c4584 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 17:54:47 +0200 Subject: [PATCH 227/865] Make the import popup closeable by clicking outside if it is finished. --- Penumbra/Import/TexToolsImporter.Gui.cs | 23 ++++++++++------------- Penumbra/UI/ImportPopup.cs | 11 ++++++++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 309f107a..a069204c 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -25,20 +25,22 @@ public partial class TexToolsImporter if (_modPackCount == 0) { ImGuiUtil.Center("Nothing to extract."); - return false; + return true; } if (_modPackCount == _currentModPackIdx) - return DrawEndState(); + { + DrawEndState(); + return true; + } ImGui.NewLine(); var percentage = (float)_currentModPackIdx / _modPackCount; ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}"); ImGui.NewLine(); - if (State == ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); - else - ImGui.TextUnformatted($"Extracting {_currentModName}..."); + ImGui.TextUnformatted(State == ImporterState.DeduplicatingFiles + ? $"Deduplicating {_currentModName}..." + : $"Extracting {_currentModName}..."); if (_currentNumOptions > 1) { @@ -63,18 +65,15 @@ public partial class TexToolsImporter } - private bool DrawEndState() + private void DrawEndState() { var success = ExtractedMods.Count(t => t.Error == null); - if (ImGui.IsKeyPressed(ImGuiKey.Escape)) - return true; - ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files."); ImGui.NewLine(); using var table = ImRaii.Table("##files", 2); if (!table) - return false; + return; foreach (var (file, dir, ex) in ExtractedMods) { @@ -93,8 +92,6 @@ public partial class TexToolsImporter ImGuiUtil.HoverTooltip(ex.ToString()); } } - - return false; } public bool DrawCancelButton(Vector2 size) diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index fb2028b5..28767edc 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -1,4 +1,6 @@ +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; @@ -68,13 +70,16 @@ public sealed class ImportPopup : Window, IUiService ImGui.SetNextWindowSize(size); using var popup = ImRaii.Popup(importPopup, ImGuiWindowFlags.Modal); PopupWasDrawn = true; + var terminate = false; using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) { - if (child) - import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); + if (child.Success && import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight()))) + if (!ImGui.IsMouseHoveringRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize()) + && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + terminate = true; } - var terminate = import.State == ImporterState.Done + terminate |= import.State == ImporterState.Done ? ImGui.Button("Close", -Vector2.UnitX) : import.DrawCancelButton(-Vector2.UnitX); if (terminate) From d815266ed7b92ce2c474cd8e4da9c36a2d7fdcf4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 12 Jul 2024 15:56:58 +0000 Subject: [PATCH 228/865] [CI] Updating repo.json for testing_1.2.0.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7e90f7a9..51e458a8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.4", + "TestingAssemblyVersion": "1.2.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,7 +18,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From d6f61f06cbf16e23900b4f317e5ecc8f63c7fc1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 12 Jul 2024 22:15:00 +0200 Subject: [PATCH 229/865] Add TestingDalamudApiLevel --- repo.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 51e458a8..59daa590 100644 --- a/repo.json +++ b/repo.json @@ -9,9 +9,10 @@ "TestingAssemblyVersion": "1.2.0.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 10, + "DalamudApiLevel": 9, + "TestingDalamudApiLevel": 10, "IsHide": "False", - "IsTestingExclusive": "True", + "IsTestingExclusive": "False", "DownloadCount": 0, "LastUpdate": 0, "LoadPriority": 69420, From e46fcc4af1bbf083ab5ce07edea67ad5a554589d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 14 Jul 2024 20:38:54 +0200 Subject: [PATCH 230/865] Gracefully deal with invalid offhand IMCs. --- Penumbra.GameData | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 3 ++- Penumbra/Services/ValidityChecker.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a64a30bf..2c067b4f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a64a30bf29cf297285ecde0579830b4d7fbae2d9 +Subproject commit 2c067b4f3c1d84888c2b961a93fe2de01fffe5f1 diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 44c60942..d4887fe2 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -100,7 +100,8 @@ public readonly record struct ImcIdentifier( return false; if (!Enum.IsDefined(ObjectType)) return false; - + if (ItemData.AdaptOffhandImc(PrimaryId, out _)) + return false; break; } diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index cefee139..5feeab02 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -45,7 +45,7 @@ public class ValidityChecker : IService public void LogExceptions() { if (ImcExceptions.Count > 0) - Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", + Penumbra.Messager.NotificationMessage($"{ImcExceptions.Count} IMC Exceptions thrown during Penumbra load. Please repair your game files.", NotificationType.Warning); } From 07c3be641ddc7950a3a818a44b5d155e828a3b7d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 14 Jul 2024 18:41:39 +0000 Subject: [PATCH 231/865] [CI] Updating repo.json for testing_1.2.0.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 59daa590..4e1b17ba 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.5", + "TestingAssemblyVersion": "1.2.0.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 9c781f8563fb0bc85247fb83aab79980943cb2d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:04:36 +0200 Subject: [PATCH 232/865] Disable material migration for now --- Penumbra.GameData | 2 +- Penumbra/Services/MigrationManager.cs | 22 ++++++++++----- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 28 ++++++++++--------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2c067b4f..c2a4c4ee 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2c067b4f3c1d84888c2b961a93fe2de01fffe5f1 +Subproject commit c2a4c4ee7470c5afbd3dd7731697ab49c055d1e3 diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 5b353912..7115fe4d 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -233,6 +233,8 @@ public class MigrationManager(Configuration config) : IService /// Writes or migrates a .mdl file during extraction from a regular archive. public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { + // TODO reactivate when this works. + return; if (!config.MigrateImportedModelsToV6) { reader.WriteEntryToDirectory(directory, options); @@ -265,6 +267,8 @@ public class MigrationManager(Configuration config) : IService public void MigrateMtrlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { + // TODO reactivate when this works. + return; if (!config.MigrateImportedMaterialsToLegacy) { reader.WriteEntryToDirectory(directory, options); @@ -276,16 +280,20 @@ public class MigrationManager(Configuration config) : IService using var e = reader.OpenEntryStream(); e.CopyTo(s); var file = new MtrlFile(s.GetBuffer()); - if (!file.IsDawnTrail) - { - file.MigrateToDawntrail(); - Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); - } Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); - s.Seek(0, SeekOrigin.Begin); - s.WriteTo(f); + if (file.IsDawnTrail) + { + file.MigrateToDawntrail(); + Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); + f.Write(file.Write()); + } + else + { + s.Seek(0, SeekOrigin.Begin); + s.WriteTo(f); + } } /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index d588eaa0..a4a2010f 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -22,10 +22,11 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura DrawMdlMigration(); DrawMdlRestore(); DrawMdlCleanup(); - ImGui.Separator(); - DrawMtrlMigration(); - DrawMtrlRestore(); - DrawMtrlCleanup(); + // TODO enable when this works + //ImGui.Separator(); + //DrawMtrlMigration(); + //DrawMtrlRestore(); + //DrawMtrlCleanup(); } private void DrawSettings() @@ -39,15 +40,16 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura ImUtf8.HoverTooltip("This increments the version marker and restructures the bone table to the new version."u8); - value = config.MigrateImportedMaterialsToLegacy; - if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) - { - config.MigrateImportedMaterialsToLegacy = value; - config.Save(); - } - - ImUtf8.HoverTooltip( - "This currently only increases the color-table size and switches the shader from 'character.shpk' to 'characterlegacy.shpk', if the former is used."u8); + // TODO enable when this works + //value = config.MigrateImportedMaterialsToLegacy; + //if (ImUtf8.Checkbox("Automatically Migrate Materials to Dawntrail on Import"u8, ref value)) + //{ + // config.MigrateImportedMaterialsToLegacy = value; + // config.Save(); + //} + // + //ImUtf8.HoverTooltip( + // "This currently only increases the color-table size and switches the shader from 'character.shpk' to 'characterlegacy.shpk', if the former is used."u8); ImUtf8.Checkbox("Create Backups During Manual Migration", ref _createBackups); } From 12dfaaef99192edfd00302d2c249f31f0c7a5509 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:05:10 +0200 Subject: [PATCH 233/865] Fix broken mods being deleted instead of removed. Fix tags crashing when null instead of empty. --- Penumbra/Mods/Manager/ModDataEditor.cs | 4 ++-- Penumbra/Mods/Manager/ModManager.cs | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 91ae4a4c..7a0467d0 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -59,7 +59,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = json[nameof(Mod.LocalTags)]?.Values().OfType() ?? localTags; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; save = false; } catch (Exception e) @@ -119,7 +119,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; var importDate = json[nameof(Mod.ImportDate)]?.Value(); - var modTags = json[nameof(Mod.ModTags)]?.Values().OfType(); + var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); ModDataChangeType changes = 0; if (mod.Name != newName) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 4b19ea4c..f170a31b 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -115,12 +115,21 @@ public sealed class ModManager : ModStorage, IDisposable, IService Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); } + RemoveMod(mod); + } + + /// + /// Remove a loaded mod. The event is invoked before the mod is removed from the list. + /// Does not delete the mod from the filesystem. + /// Updates indices of later mods. + /// + public void RemoveMod(Mod mod) + { _communicator.ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); foreach (var remainingMod in Mods.Skip(mod.Index + 1)) --remainingMod.Index; Mods.RemoveAt(mod.Index); - - Penumbra.Log.Debug($"Deleted mod {mod.Name}."); + Penumbra.Log.Debug($"Removed loaded mod {mod.Name} from list."); } /// @@ -135,10 +144,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService if (!Creator.ReloadMod(mod, true, out var metaChange)) { Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - - DeleteMod(mod); + ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); + RemoveMod(mod); return; } From d952d83adf1b61ccb664420b29e7eb81593d3f7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:06:02 +0200 Subject: [PATCH 234/865] Fix redrawing while fishing while sitting. --- Penumbra/Interop/Services/RedrawService.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 163b2c0e..f288a35e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -301,9 +301,14 @@ public sealed unsafe partial class RedrawService : IDisposable (CharacterModes)6 => // fishing GetCurrentAnimationId(obj) switch { - 278 => true, // line out. - 283 => true, // reeling in - _ => false, + 278 => true, // line out. + 283 => true, // reeling in + 284 => true, // reeling in + 287 => true, // reeling in 2 + 3149 => true, // line out sitting, + 3155 => true, // reeling in sitting, + 3159 => true, // reeling in sitting 2, + _ => false, }, _ => false, }; @@ -419,7 +424,7 @@ public sealed unsafe partial class RedrawService : IDisposable if (housingManager == null) return; - var currentTerritory = (OutdoorTerritory*) housingManager->CurrentTerritory; + var currentTerritory = (OutdoorTerritory*)housingManager->CurrentTerritory; if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Outdoor) return; From c98bee67a5bb28adf2f59efbb23968f862d7653a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:25:28 +0200 Subject: [PATCH 235/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c2a4c4ee..67109fa9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c2a4c4ee7470c5afbd3dd7731697ab49c055d1e3 +Subproject commit 67109fa9e89d5ff5c9f93705208db92e836e9ef4 From 78af40d5078dc322174935c76606256fcc08fdc1 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jul 2024 20:27:30 +0000 Subject: [PATCH 236/865] [CI] Updating repo.json for testing_1.2.0.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4e1b17ba..9f11c118 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.6", + "TestingAssemblyVersion": "1.2.0.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 519b3d4891c67650d3a333fe9d52f77240f989f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 22:36:55 +0200 Subject: [PATCH 237/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 67109fa9..c9e0d890 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 67109fa9e89d5ff5c9f93705208db92e836e9ef4 +Subproject commit c9e0d8905137fef6ed7c247ee1d2824d3f89f3b2 From 89cbb3f60dde8e72f3b4b8e2887eae5701268d39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 16 Jul 2024 23:02:17 +0200 Subject: [PATCH 238/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c9e0d890..45f2c901 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c9e0d8905137fef6ed7c247ee1d2824d3f89f3b2 +Subproject commit 45f2c901b3a0131eaee18b3520184baeb0d1049d From eb784dddf04e4380db02df6f0a58b95bd2635db8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 00:40:49 +0200 Subject: [PATCH 239/865] Fix missing file display. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 83a8958b..e915a879 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -132,7 +132,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService sb.Append($" | {unused} Unused Files"); if (_editor.Files.Missing.Count > 0) - sb.Append($" | {_editor.Files.Available.Count} Missing Files"); + sb.Append($" | {_editor.Files.Missing.Count} Missing Files"); if (redirections > 0) sb.Append($" | {redirections} Redirections"); From 67a35b9abbb22576dc8d5c5d16e7201d3fbb4fa9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 00:49:40 +0200 Subject: [PATCH 240/865] stupid --- Penumbra/Services/MigrationManager.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 7115fe4d..84318da6 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -233,8 +233,6 @@ public class MigrationManager(Configuration config) : IService /// Writes or migrates a .mdl file during extraction from a regular archive. public void MigrateMdlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { - // TODO reactivate when this works. - return; if (!config.MigrateImportedModelsToV6) { reader.WriteEntryToDirectory(directory, options); @@ -267,9 +265,7 @@ public class MigrationManager(Configuration config) : IService public void MigrateMtrlDuringExtraction(IReader reader, string directory, ExtractionOptions options) { - // TODO reactivate when this works. - return; - if (!config.MigrateImportedMaterialsToLegacy) + if (!config.MigrateImportedMaterialsToLegacy || true) // TODO change when this is working { reader.WriteEntryToDirectory(directory, options); return; @@ -327,7 +323,7 @@ public class MigrationManager(Configuration config) : IService /// Update the data of a .mtrl file during TTMP extraction. Returns either the existing array or a new one. public byte[] MigrateTtmpMaterial(string path, byte[] data) { - if (!config.MigrateImportedMaterialsToLegacy) + if (!config.MigrateImportedMaterialsToLegacy || true) // TODO fix when this is working return data; try From ad877e68e610fcdec3f7fee993154a74b8929d3b Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jul 2024 22:51:33 +0000 Subject: [PATCH 241/865] [CI] Updating repo.json for testing_1.2.0.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9f11c118..850acf1b 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.7", + "TestingAssemblyVersion": "1.2.0.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 4824a96ab0f9854ab75248c38a3f7549b1aad97a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 01:35:56 +0200 Subject: [PATCH 242/865] Enable Mtrl Restore and Cleanup again. --- Penumbra/UI/Classes/MigrationSectionDrawer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/UI/Classes/MigrationSectionDrawer.cs b/Penumbra/UI/Classes/MigrationSectionDrawer.cs index a4a2010f..a3dcd23a 100644 --- a/Penumbra/UI/Classes/MigrationSectionDrawer.cs +++ b/Penumbra/UI/Classes/MigrationSectionDrawer.cs @@ -23,10 +23,10 @@ public class MigrationSectionDrawer(MigrationManager migrationManager, Configura DrawMdlRestore(); DrawMdlCleanup(); // TODO enable when this works - //ImGui.Separator(); + ImGui.Separator(); //DrawMtrlMigration(); - //DrawMtrlRestore(); - //DrawMtrlCleanup(); + DrawMtrlRestore(); + DrawMtrlCleanup(); } private void DrawSettings() From c9379b6d60d2a1f7f133c9fa7da3d7a6952a3651 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 16 Jul 2024 23:38:11 +0000 Subject: [PATCH 243/865] [CI] Updating repo.json for testing_1.2.0.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 850acf1b..b4becf0f 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.8", + "TestingAssemblyVersion": "1.2.0.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1922353ba30e05a2b862f88b74a5e311fe5eb6d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 02:01:52 +0200 Subject: [PATCH 244/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 45f2c901..c25ea7b1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 45f2c901b3a0131eaee18b3520184baeb0d1049d +Subproject commit c25ea7b19a6db37dd36e12b9a7a71f72a192ab57 From e7c786b239a53b3e145cc1e1101324170eb883b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 18:02:48 +0200 Subject: [PATCH 245/865] Add and rework hooks around EST entries. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 47 +++++++++++-------- .../Hooks/Resources/ResolvePathHooksBase.cs | 38 ++++++++++++++- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c25ea7b1..94df458d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c25ea7b19a6db37dd36e12b9a7a71f72a192ab57 +Subproject commit 94df458dfb2a704a611fa77d955808284aeb23ac diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 5b272019..ce002664 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -3,51 +3,58 @@ using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; using Penumbra.Meta.Manipulations; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Interop.Hooks.Meta; -public class EstHook : FastHook, IDisposable +public unsafe class EstHook : FastHook, IDisposable { - public delegate EstEntry Delegate(uint id, int estType, uint genderRace); + public delegate EstEntry Delegate(ResourceHandle* estResource, uint id, uint genderRace); - private readonly MetaState _metaState; + private readonly CharacterUtility _characterUtility; + private readonly MetaState _metaState; - public EstHook(HookManager hooks, MetaState metaState) + public EstHook(HookManager hooks, MetaState metaState, CharacterUtility characterUtility) { - _metaState = metaState; - Task = hooks.CreateHook("GetEstEntry", Sigs.GetEstEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); + _metaState = metaState; + _characterUtility = characterUtility; + Task = hooks.CreateHook("FindEstEntry", Sigs.FindEstEntry, Detour, + metaState.Config.EnableMods && HookSettings.MetaEntryHooks); _metaState.Config.ModsEnabled += Toggle; } - private EstEntry Detour(uint genderRace, int estType, uint id) + private EstEntry Detour(ResourceHandle* estResource, uint genderRace, uint id) { EstEntry ret; if (_metaState.EstCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache } - && cache.Est.TryGetValue(Convert(genderRace, estType, id), out var entry)) + && cache.Est.TryGetValue(Convert(estResource, genderRace, id), out var entry)) ret = entry.Entry; else - ret = Task.Result.Original(genderRace, estType, id); + ret = Task.Result.Original(estResource, genderRace, id); - Penumbra.Log.Excessive($"[GetEstEntry] Invoked with {genderRace}, {estType}, {id}, returned {ret.Value}."); + Penumbra.Log.Information($"[FindEstEntry] Invoked with 0x{(nint)estResource:X}, {genderRace}, {id}, returned {ret.Value}."); return ret; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static EstIdentifier Convert(uint genderRace, int estType, uint id) + private EstIdentifier Convert(ResourceHandle* estResource, uint genderRace, uint id) { var i = new PrimaryId((ushort)id); var gr = (GenderRace)genderRace; - var type = estType switch - { - 1 => EstType.Face, - 2 => EstType.Hair, - 3 => EstType.Head, - 4 => EstType.Body, - _ => (EstType)0, - }; - return new EstIdentifier(i, type, gr); + + if (estResource == _characterUtility.Address->BodyEstResource) + return new EstIdentifier(i, EstType.Body, gr); + if (estResource == _characterUtility.Address->HairEstResource) + return new EstIdentifier(i, EstType.Hair, gr); + if (estResource == _characterUtility.Address->FaceEstResource) + return new EstIdentifier(i, EstType.Face, gr); + if (estResource == _characterUtility.Address->HeadEstResource) + return new EstIdentifier(i, EstType.Head, gr); + + return new EstIdentifier(i, 0, gr); } public void Dispose() diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 8fa6d861..e1b6e46e 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -19,6 +19,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private delegate nint NamedResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint name); private delegate nint PerSlotResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex); private delegate nint SingleResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize); + private delegate nint SkeletonVFuncDelegate(nint drawObject, int estType, nint unk); private delegate nint TmbResolveDelegate(nint drawObject, nint pathBuffer, nint pathBufferSize, nint timelineName); @@ -37,6 +38,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private readonly Hook _resolveSkpPathHook; private readonly Hook _resolveTmbPathHook; private readonly Hook _resolveVfxPathHook; + private readonly Hook? _vFunc81Hook; + private readonly Hook? _vFunc83Hook; private readonly PathState _parent; @@ -49,6 +52,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook = Create($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman); _resolvePhybPathHook = Create($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman); + _vFunc81Hook = Create( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81); + + _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); @@ -58,6 +64,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _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 if (HookSettings.ResourceHooks) Enable(); @@ -77,6 +85,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook.Enable(); _resolveTmbPathHook.Enable(); _resolveVfxPathHook.Enable(); + _vFunc81Hook?.Enable(); + _vFunc83Hook?.Enable(); } public void Disable() @@ -93,6 +103,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook.Disable(); _resolveTmbPathHook.Disable(); _resolveVfxPathHook.Disable(); + _vFunc81Hook?.Disable(); + _vFunc83Hook?.Disable(); } public void Dispose() @@ -109,6 +121,8 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveSkpPathHook.Dispose(); _resolveTmbPathHook.Dispose(); _resolveVfxPathHook.Dispose(); + _vFunc81Hook?.Dispose(); + _vFunc83Hook?.Dispose(); } private nint ResolveDecal(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex) @@ -224,14 +238,36 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ResolvePath(drawObject, pathBuffer); } + private nint VFunc81(nint drawObject, int estType, nint unk) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = _vFunc81Hook!.Original(drawObject, estType, unk); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + + private nint VFunc83(nint drawObject, int estType, nint unk) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = _vFunc83Hook!.Original(drawObject, estType, unk); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static Hook Create(string name, HookManager hooks, nint address, Type type, T other, T human) where T : Delegate + [return: NotNullIfNotNull(nameof(other))] + private static Hook? Create(string name, HookManager hooks, nint address, Type type, T? other, T human) where T : Delegate { var del = type switch { Type.Human => human, _ => other, }; + if (del == null) + return null; + return hooks.CreateHook(name, address, del).Result; } From 6d0562180acbae396cbcbb8c9ed21f0d1b1eec2c Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 17 Jul 2024 16:05:28 +0000 Subject: [PATCH 246/865] [CI] Updating repo.json for testing_1.2.0.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index b4becf0f..986c1707 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.9", + "TestingAssemblyVersion": "1.2.0.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 9bba1e2b31a26e890e34d72693e7d2521e43fea3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 18:05:57 +0200 Subject: [PATCH 247/865] Remove log spamming. --- Penumbra/Interop/Hooks/Meta/EstHook.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index ce002664..825b1244 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -35,7 +35,7 @@ public unsafe class EstHook : FastHook, IDisposable else ret = Task.Result.Original(estResource, genderRace, id); - Penumbra.Log.Information($"[FindEstEntry] Invoked with 0x{(nint)estResource:X}, {genderRace}, {id}, returned {ret.Value}."); + Penumbra.Log.Excessive($"[FindEstEntry] Invoked with 0x{(nint)estResource:X}, {genderRace}, {id}, returned {ret.Value}."); return ret; } From 5abbd8b1101090afba2729cfbbf4adeeb673e615 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Jul 2024 18:34:03 +0200 Subject: [PATCH 248/865] Hook UpdateRender despite per-frame calls. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs | 37 ------------------- .../{GetEqpIndirect2.cs => UpdateRender.cs} | 15 +++----- 3 files changed, 6 insertions(+), 48 deletions(-) delete mode 100644 Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs rename Penumbra/Interop/Hooks/Meta/{GetEqpIndirect2.cs => UpdateRender.cs} (55%) diff --git a/Penumbra.GameData b/Penumbra.GameData index 94df458d..c5ad1f3a 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 94df458dfb2a704a611fa77d955808284aeb23ac +Subproject commit c5ad1f3ae9818baa446327bdcf49fac65088c703 diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs b/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs deleted file mode 100644 index 8bd49500..00000000 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs +++ /dev/null @@ -1,37 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Meta; - -public sealed unsafe class GetEqpIndirect : FastHook -{ - private readonly CollectionResolver _collectionResolver; - private readonly MetaState _metaState; - - public GetEqpIndirect(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) - { - _collectionResolver = collectionResolver; - _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, HookSettings.MetaParentHooks); - } - - public delegate void Delegate(DrawObject* drawObject); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void Detour(DrawObject* drawObject) - { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if ((*(byte*)((nint)drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)((nint)drawObject + Offsets.GetEqpIndirectSkip2) == 0) - return; - - Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}."); - var collection = _collectionResolver.IdentifyCollection(drawObject, true); - _metaState.EqpCollection.Push(collection); - Task.Result.Original(drawObject); - _metaState.EqpCollection.Pop(); - } -} diff --git a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs similarity index 55% rename from Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs rename to Penumbra/Interop/Hooks/Meta/UpdateRender.cs index e90674a8..95cc0e15 100644 --- a/Penumbra/Interop/Hooks/Meta/GetEqpIndirect2.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs @@ -1,20 +1,20 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; -using Penumbra.GameData; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Meta; -public sealed unsafe class GetEqpIndirect2 : FastHook +/// The actual function is inlined, so we need to hook its only callsite: Human.UpdateRender instead. +public sealed unsafe class UpdateRender : FastHook { private readonly CollectionResolver _collectionResolver; private readonly MetaState _metaState; - public GetEqpIndirect2(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) + public UpdateRender(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vTables) { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Get EQP Indirect 2", Sigs.GetEqpIndirect2, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Human.UpdateRender", vTables.HumanVTable[4], Detour, HookSettings.MetaParentHooks); } public delegate void Delegate(DrawObject* drawObject); @@ -22,12 +22,7 @@ public sealed unsafe class GetEqpIndirect2 : FastHook [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void Detour(DrawObject* drawObject) { - // Shortcut because this is also called all the time. - // Same thing is checked at the beginning of the original function. - if (((*(uint*)((nint)drawObject + Offsets.GetEqpIndirect2Skip) >> 0x12) & 1) == 0) - return; - - Penumbra.Log.Excessive($"[Get EQP Indirect 2] Invoked on {(nint)drawObject:X}."); + Penumbra.Log.Excessive($"[Human.UpdateRender] Invoked on {(nint)drawObject:X}."); var collection = _collectionResolver.IdentifyCollection(drawObject, true); _metaState.EqpCollection.Push(collection); Task.Result.Original(drawObject); From defba19b2d7714162d0eca036d212aef48aad667 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 17 Jul 2024 16:36:04 +0000 Subject: [PATCH 249/865] [CI] Updating repo.json for testing_1.2.0.11 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 986c1707..d715bca2 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.10", + "TestingAssemblyVersion": "1.2.0.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.11/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From f978b35b764fac7037f72f9ffa6f60dc2cd7bf13 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jul 2024 14:24:29 +0200 Subject: [PATCH 250/865] Make ResourceTrees work with UseNoMods. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/MetaCache.cs | 4 ---- .../Collections/ModCollection.Cache.Access.cs | 9 --------- Penumbra/Import/Models/ModelManager.cs | 6 ++++-- .../ResourceTree/ResolveContext.PathResolution.cs | 15 ++++++++------- Penumbra/Interop/ResourceTree/ResolveContext.cs | 10 ++++++++-- .../Interop/ResourceTree/ResourceTreeFactory.cs | 4 +++- 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c5ad1f3a..a1e637f8 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c5ad1f3ae9818baa446327bdcf49fac65088c703 +Subproject commit a1e637f835c1a42732825e8e0690aeef0024b101 diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 02056fad..1a6924a9 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -103,10 +103,6 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ~MetaCache() => Dispose(); - /// Try to obtain a manipulated IMC file. - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file) - => Imc.GetFile(path.Path, out file); - internal EqdpEntry GetEqdpEntry(GenderRace race, bool accessory, PrimaryId primaryId) => Eqdp.ApplyFullEntry(primaryId, race, accessory, Meta.Files.ExpandedEqdpFile.GetDefault(manager, race, accessory, primaryId)); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 81751128..983509a4 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -43,15 +43,6 @@ public partial class ModCollection internal MetaCache? MetaCache => _cache?.Meta; - public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file) - { - if (_cache != null) - return _cache.Meta.GetImcFile(path, out file); - - file = null; - return false; - } - internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 01396cfb..0c19bc0a 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -12,6 +12,8 @@ using Penumbra.GameData.Structs; using Penumbra.Import.Models.Export; using Penumbra.Import.Models.Import; using Penumbra.Import.Textures; +using Penumbra.Meta; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using SharpGLTF.Scenes; using SixLabors.ImageSharp; @@ -22,7 +24,7 @@ namespace Penumbra.Import.Models; using Schema2 = SharpGLTF.Schema2; using LuminaMaterial = Lumina.Models.Materials.Material; -public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) +public sealed class ModelManager(IFramework framework, MetaFileManager metaFileManager, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable, IService { private readonly IFramework _framework = framework; @@ -97,7 +99,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Value ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? EstEntry.Zero; + ?? EstFile.GetDefault(metaFileManager, type, info.GenderRace, info.PrimaryId); // If there's no entries, we can assume that there's no additional skeleton. if (targetId == EstEntry.Zero) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 72cb1681..07f305ac 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -3,6 +3,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String; @@ -51,10 +52,8 @@ internal partial record ResolveContext return GenderRace.MidlanderMale; var metaCache = Global.Collection.MetaCache; - if (metaCache == null) - return GenderRace.MidlanderMale; - - var entry = metaCache.GetEqdpEntry(characterRaceCode, accessory, primaryId); + var entry = metaCache?.GetEqdpEntry(characterRaceCode, accessory, primaryId) + ?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, characterRaceCode, accessory, primaryId); if (entry.ToBits(slot).Item2) return characterRaceCode; @@ -62,7 +61,8 @@ internal partial record ResolveContext if (fallbackRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - entry = metaCache.GetEqdpEntry(fallbackRaceCode, accessory, primaryId); + entry = metaCache?.GetEqdpEntry(fallbackRaceCode, accessory, primaryId) + ?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, fallbackRaceCode, accessory, primaryId); if (entry.ToBits(slot).Item2) return fallbackRaceCode; @@ -271,8 +271,9 @@ internal partial record ResolveContext private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, PrimaryId primary) { - var metaCache = Global.Collection.MetaCache; - var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) ?? default; + var metaCache = Global.Collection.MetaCache; + var skeletonSet = metaCache?.GetEstEntry(type, raceCode, primary) + ?? EstFile.GetDefault(Global.MetaFileManager, type, raceCode, primary); return (raceCode, type.ToName(), skeletonSet.AsId); } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index a852a4cc..acb320d4 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.PathResolving; +using Penumbra.Meta; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; @@ -19,7 +20,12 @@ using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.M namespace Penumbra.Interop.ResourceTree; -internal record GlobalResolveContext(ObjectIdentification Identifier, ModCollection Collection, TreeBuildCache TreeBuildCache, bool WithUiData) +internal record GlobalResolveContext( + MetaFileManager MetaFileManager, + ObjectIdentification Identifier, + ModCollection Collection, + TreeBuildCache TreeBuildCache, + bool WithUiData) { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); @@ -111,7 +117,7 @@ internal unsafe partial record ResolveContext( if (resourceHandle == null) throw new ArgumentNullException(nameof(resourceHandle)); - var fileName = (ReadOnlySpan) resourceHandle->FileName.AsSpan(); + var fileName = (ReadOnlySpan)resourceHandle->FileName.AsSpan(); var additionalData = ByteString.Empty; if (PathDataHandler.Split(fileName, out fileName, out var data)) additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 1f6d1f6f..46c7ce35 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -8,6 +8,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; +using Penumbra.Meta; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -15,6 +16,7 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceTreeFactory( IDataManager gameData, ObjectManager objects, + MetaFileManager metaFileManager, CollectionResolver resolver, ObjectIdentification identifier, Configuration config, @@ -78,7 +80,7 @@ public class ResourceTreeFactory( var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); - var globalContext = new GlobalResolveContext(identifier, collectionResolveData.ModCollection, + var globalContext = new GlobalResolveContext(metaFileManager, identifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) { From f533ae66671c49682451982656f268d13192d7a0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Jul 2024 17:33:54 +0200 Subject: [PATCH 251/865] Some cleanup. --- .../ResolveContext.PathResolution.cs | 2 -- .../ResourceTree/ResourceTreeFactory.cs | 18 ++++++++++-------- .../Interop/Structs/CharacterUtilityData.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 07f305ac..678dd8a9 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -199,8 +199,6 @@ internal partial record ResolveContext ByteString? path; try { - Penumbra.Log.Information($"{(nint)CharacterBase:X} {ModelType} {SlotIndex} 0x{(ulong)mtrlFileName:X}"); - Penumbra.Log.Information($"{new ByteString(mtrlFileName)}"); path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); } catch (AccessViolationException) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 46c7ce35..65fac68f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -18,7 +18,7 @@ public class ResourceTreeFactory( ObjectManager objects, MetaFileManager metaFileManager, CollectionResolver resolver, - ObjectIdentification identifier, + ObjectIdentification objectIdentifier, Configuration config, ActorManager actors, PathState pathState) : IService @@ -80,7 +80,7 @@ public class ResourceTreeFactory( var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); - var globalContext = new GlobalResolveContext(metaFileManager, identifier, collectionResolveData.ModCollection, + var globalContext = new GlobalResolveContext(metaFileManager, objectIdentifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) { @@ -125,6 +125,14 @@ public class ResourceTreeFactory( private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath) { + foreach (var node in tree.FlatNodes) + { + if (!ShallKeepPath(node.FullPath, onlyWithinPath)) + node.FullPath = FullPath.Empty; + } + + return; + static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath) { if (!fullPath.IsRooted) @@ -139,12 +147,6 @@ public class ResourceTreeFactory( return fullPath.Exists; } - - foreach (var node in tree.FlatNodes) - { - if (!ShallKeepPath(node.FullPath, onlyWithinPath)) - node.FullPath = FullPath.Empty; - } } private static void Cleanup(ResourceTree tree) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index d33da477..197de0bb 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -15,7 +15,7 @@ public unsafe struct CharacterUtilityData .Where(n => n.First.StartsWith("Eqdp")) .Select(n => n.Second).ToArray(); - public const int TotalNumResources = 89; + public const int TotalNumResources = 114; /// Obtain the index for the eqdp file corresponding to the given race code and accessory. public static MetaIndex EqdpIdx(GenderRace raceCode, bool accessory) From a4548bbf0426fb181e49396147677ebecfe87147 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:11:13 +0200 Subject: [PATCH 252/865] Apply unprioritized mod groups in reverse order. --- Penumbra/Mods/Mod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index fcea7133..16f06de2 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -73,7 +73,7 @@ public sealed class Mod : IMod var dictRedirections = new Dictionary(TotalFileCount); var setManips = new MetaDictionary(); - foreach (var (group, groupIndex) in Groups.WithIndex().OrderByDescending(g => g.Value.Priority)) + foreach (var (group, groupIndex) in Groups.WithIndex().Reverse().OrderByDescending(g => g.Value.Priority)) { var config = settings.Settings[groupIndex]; group.AddData(config, dictRedirections, setManips); From 258f7e9732229f754f086fa79126e4246a7eb0b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:13:43 +0200 Subject: [PATCH 253/865] Reinstate the inlined ApricotSoundPlay hook one layer hup. --- Penumbra.GameData | 2 +- .../Animation/ApricotListenerSoundPlay.cs | 42 ++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a1e637f8..9f1816f1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a1e637f835c1a42732825e8e0690aeef0024b101 +Subproject commit 9f1816f1b75003d01c5576769831c10f3d8948a7 diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 2e05c1b6..361fcd4e 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -1,4 +1,3 @@ -using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; @@ -11,33 +10,48 @@ using Penumbra.Services; namespace Penumbra.Interop.Hooks.Animation; /// Called for some sound effects caused by animations or VFX. -public sealed unsafe class ApricotListenerSoundPlay : FastHook +/// Actual function got inlined. +public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook { private readonly GameState _state; private readonly CollectionResolver _collectionResolver; private readonly CrashHandlerService _crashHandler; - // TODO because of inlining. - public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) + public ApricotListenerSoundPlayCaller(HookManager hooks, GameState state, CollectionResolver collectionResolver, + CrashHandlerService crashHandler) { _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Apricot Listener Sound Play Caller", Sigs.ApricotListenerSoundPlayCaller, Detour, + true); //HookSettings.VfxIdentificationHooks); } - public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6); + public delegate nint Delegate(nint a1, nint a2, float a3); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private nint Detour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6) + private nint Detour(nint a1, nint unused, float timeOffset) { - Penumbra.Log.Excessive($"[Apricot Listener Sound Play] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}, {a5}, {a6}."); - if (a6 == nint.Zero) - return Task.Result.Original(a1, a2, a3, a4, a5, a6); + // Short-circuiting and sanity checks done by game. + var playTime = a1 == nint.Zero ? -1 : *(float*)(a1 + 0x250); + if (playTime < 0) + return Task.Result.Original(a1, unused, timeOffset); - // a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1. - var gameObject = (*(delegate* unmanaged**)a6)[1](a6); + var someIntermediate = *(nint*)(a1 + 0x1F8); + var flags = someIntermediate == nint.Zero ? (ushort)0 : *(ushort*)(someIntermediate + 0x49C); + if (((flags >> 13) & 1) == 0) + return Task.Result.Original(a1, unused, timeOffset); + + Penumbra.Log.Information( + $"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}."); + // Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay) + var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270); + if (apricotIInstanceListenner == nint.Zero) + return Task.Result.Original(a1, unused, timeOffset); + + // In some cases we can obtain the associated caster via vfunc 1. var newData = ResolveData.Invalid; + var gameObject = (*(delegate* unmanaged**)apricotIInstanceListenner)[1](apricotIInstanceListenner); if (gameObject != null) { newData = _collectionResolver.IdentifyCollection(gameObject, true); @@ -47,14 +61,14 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook Date: Sun, 21 Jul 2024 00:21:27 +0200 Subject: [PATCH 254/865] Fix field. --- .../UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 15bd7cc9..25c0e448 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -75,7 +75,7 @@ public partial class ModEditWindow ImGui.TableHeader("Dye Preview"); } - for (var i = 0; i < ColorTable.NumUsedRows; ++i) + for (var i = 0; i < ColorTable.NumRows; ++i) { ret |= DrawColorTableRow(tab, i, disabled); ImGui.TableNextRow(); From 5b1c0cf0e3206fff550060f97375aeb8efae7aee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:21:41 +0200 Subject: [PATCH 255/865] Fix direction of furniture redrawing. --- Penumbra/Interop/Services/RedrawService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index f288a35e..2cdc1137 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -424,8 +424,8 @@ public sealed unsafe partial class RedrawService : IDisposable if (housingManager == null) return; - var currentTerritory = (OutdoorTerritory*)housingManager->CurrentTerritory; - if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Outdoor) + var currentTerritory = (IndoorTerritory*)housingManager->CurrentTerritory; + if (currentTerritory == null || currentTerritory->GetTerritoryType() is not HousingTerritoryType.Indoor) return; From c3b7ddad2810e4687aec33b4da7f721de3b4625f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 00:48:48 +0200 Subject: [PATCH 256/865] Create newly added mods in import folder instead of moving them. --- OtterGui | 2 +- Penumbra/Mods/Manager/ModFileSystem.cs | 21 +++++++++++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 31 -------------------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/OtterGui b/OtterGui index 89b3b951..dc17161b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 89b3b9513f9b4989045517a452ef971e24377203 +Subproject commit dc17161b1d9c47ffd6bcc17e91f4832cf7762993 diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index e32fec0c..693db944 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,3 +1,5 @@ +using Dalamud.Interface.ImGuiNotification; +using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Communication; @@ -10,13 +12,15 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer private readonly ModManager _modManager; private readonly CommunicatorService _communicator; private readonly SaveService _saveService; + private readonly Configuration _config; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService) + public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService, Configuration config) { _modManager = modManager; _communicator = communicator; _saveService = saveService; + _config = config; Reload(); Changed += OnChange; _communicator.ModDiscoveryFinished.Subscribe(Reload, ModDiscoveryFinished.Priority.ModFileSystem); @@ -91,7 +95,20 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer switch (type) { case ModPathChangeType.Added: - CreateDuplicateLeaf(Root, mod.Name.Text, mod); + var parent = Root; + if (_config.DefaultImportFolder.Length != 0) + try + { + parent = FindOrCreateAllFolders(_config.DefaultImportFolder); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, + $"Could not move newly imported mod {mod.Name} to default import folder {_config.DefaultImportFolder}.", + NotificationType.Warning); + } + + CreateDuplicateLeaf(parent, mod.Name.Text, mod); break; case ModPathChangeType.Deleted: if (FindLeaf(mod, out var leaf)) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 88d6afa2..55405313 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -196,10 +196,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf leaf, in ModState state, bool selected) @@ -379,34 +376,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector - /// If a default import folder is setup, try to move the given mod in there. - /// If the folder does not exist, create it if possible. - /// - /// - private void MoveModToDefaultDirectory(Mod mod) - { - if (_config.DefaultImportFolder.Length == 0) - return; - - try - { - var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) - .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); - if (leaf == null) - throw new Exception("Mod was not found at root."); - - var folder = FileSystem.FindOrCreateAllFolders(_config.DefaultImportFolder); - FileSystem.Move(leaf, folder); - } - catch (Exception e) - { - _messager.NotificationMessage(e, - $"Could not move newly imported mod {mod.Name} to default import folder {_config.DefaultImportFolder}.", - NotificationType.Warning); - } - } - private void DrawHelpPopup() { ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * UiHelpers.Scale, 38.5f * ImGui.GetTextLineHeightWithSpacing()), () => From 48ab98bee69117f5c74c425ab19d9c65fd545041 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Jul 2024 22:53:37 +0000 Subject: [PATCH 257/865] [CI] Updating repo.json for testing_1.2.0.12 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index d715bca2..ca13bb5b 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.11", + "TestingAssemblyVersion": "1.2.0.12", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.12/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8c34c18643b34586346a902f6b0ffdcca6cc2907 Mon Sep 17 00:00:00 2001 From: pmgr <26606291+pmgr@users.noreply.github.com> Date: Sun, 21 Jul 2024 16:34:27 +0100 Subject: [PATCH 258/865] Add scuffed pap handling --- .../Hooks/ResourceLoading/MappedCodeReader.cs | 13 ++ .../Hooks/ResourceLoading/PapHandler.cs | 23 +++ .../Hooks/ResourceLoading/PapRewriter.cs | 181 ++++++++++++++++ .../Hooks/ResourceLoading/PeSigScanner.cs | 194 ++++++++++++++++++ .../Hooks/ResourceLoading/ResourceLoader.cs | 26 +++ Penumbra/Penumbra.csproj | 13 +- 6 files changed, 446 insertions(+), 4 deletions(-) create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs create mode 100644 Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs diff --git a/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs new file mode 100644 index 00000000..81712cca --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs @@ -0,0 +1,13 @@ +using Iced.Intel; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +public class MappedCodeReader(UnmanagedMemoryAccessor data, long offset) : CodeReader +{ + public override int ReadByte() { + if (offset >= data.Capacity) + return -1; + + return data.ReadByte(offset++); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs new file mode 100644 index 00000000..29d77d83 --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -0,0 +1,23 @@ +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +{ + private readonly PapRewriter _papRewriter = new(papResourceHandler); + + public void Enable() + { + _papRewriter.Rewrite(Sigs.LoadAlwaysResidentMotionPacks); + _papRewriter.Rewrite(Sigs.LoadWeaponDependentResidentMotionPacks); + _papRewriter.Rewrite(Sigs.LoadInitialResidentMotionPacks); + _papRewriter.Rewrite(Sigs.LoadMotionPacks); + _papRewriter.Rewrite(Sigs.LoadMotionPacks2); + _papRewriter.Rewrite(Sigs.LoadMigratoryMotionPack); + } + + public void Dispose() + { + _papRewriter.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs new file mode 100644 index 00000000..cb437d9e --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -0,0 +1,181 @@ +using Dalamud.Hooking; +using Iced.Intel; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +{ + public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); + + private PeSigScanner Scanner { get; } = new(); + private Dictionary Hooks { get; }= []; + private List NativeAllocList { get; } = []; + private PapResourceHandlerPrototype PapResourceHandler { get; } = papResourceHandler; + + public void Rewrite(string sig) + { + if (!Scanner.TryScanText(sig, out var addr)) + { + throw new Exception($"Sig is fucked: {sig}"); + } + + var funcInstructions = Scanner.GetFunctionInstructions(addr).ToList(); + + var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); + + foreach (var hookPoint in hookPoints) + { + var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + + var stringLoc = NativeAlloc(Utf8GamePath.MaxGamePathLength); + + { + // We'll need to grab our true hook point; the location where we can change the path at our leisure. + // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. + // Pretty scuffed, this might need a refactoring at some point. + // We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL + var detourPoint = funcInstructions.Skip( + funcInstructions.FindIndex(instr => instr.IP == hookPoint.IP) + 1 + ).First(instr => instr.Mnemonic == Mnemonic.Call); + + // We'll also remove all the 'hookPoints' from 'stackAccesses'. + // We're handling the char *path redirection here, so we don't want this to hit the later code + foreach (var hp in hookPoints) + { + stackAccesses.RemoveAll(instr => instr.IP == hp.IP); + } + + var pDetour = Marshal.GetFunctionPointerForDelegate(PapResourceHandler); + var targetRegister = hookPoint.Op0Register.ToString().ToLower(); + var hookAddr = new IntPtr((long)detourPoint.IP); + + var caveLoc = NativeAlloc(16); + var hook = new AsmHook( + hookAddr, + [ + "use64", + $"mov {targetRegister}, 0x{stringLoc:x8}", // Move our char *path into the relevant register (rdx) + + // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves + // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call + // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh + $"mov r9, 0x{caveLoc:x8}", + "mov [r9], rcx", + "mov [r9+0x8], rdx", + + // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway + $"mov rax, 0x{pDetour:x8}", // Get a pointer to our detour in place + "call rax", // Call detour + + // Do the reverse process and retrieve the stored stuff + $"mov r9, 0x{caveLoc:x8}", + "mov rcx, [r9]", + "mov rdx, [r9+0x8]", + + // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call + "mov r8, rax", + ], "Pap Redirection" + ); + + Hooks.Add(hookAddr, hook); + hook.Enable(); + } + + // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' + foreach (var stackAccess in stackAccesses) + { + var hookAddr = new IntPtr((long)stackAccess.IP + stackAccess.Length); + + if (Hooks.ContainsKey(hookAddr)) + { + // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip + continue; + } + + var targetRegister = stackAccess.Op0Register.ToString().ToLower(); + var hook = new AsmHook( + hookAddr, + [ + "use64", + $"mov {targetRegister}, 0x{stringLoc:x8}", + ], "Pap Stack Accesses" + ); + + Hooks.Add(hookAddr, hook); + hook.Enable(); + } + } + + + + } + + private static IEnumerable ScanStackAccesses(IEnumerable instructions, Instruction hookPoint) + { + return instructions.Where(instr => + instr.Code == hookPoint.Code + && instr.Op0Kind == hookPoint.Op0Kind + && instr.Op1Kind == hookPoint.Op1Kind + && instr.MemoryBase == hookPoint.MemoryBase + && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) + .GroupBy(instr => instr.IP) + .Select(grp => grp.First()); + } + + // This is utterly fucked and hardcoded, but, again, it works + // Might be a neat idea for a more versatile kind of signature though + private static IEnumerable ScanPapHookPoints(List funcInstructions) + { + for (var i = 0; i < funcInstructions.Count - 8; i++) + { + if (funcInstructions[i .. (i + 8)] is + [ + {Code : Code.Lea_r64_m}, + {Code : Code.Lea_r64_m}, + {Mnemonic: Mnemonic.Call}, + {Code : Code.Lea_r64_m}, + {Mnemonic: Mnemonic.Call}, + {Code : Code.Lea_r64_m}, + .., + ] + ) + { + yield return funcInstructions[i]; + } + } + } + + private unsafe IntPtr NativeAlloc(nuint size) + { + var caveLoc = new IntPtr(NativeMemory.Alloc(size)); + NativeAllocList.Add(caveLoc); + + return caveLoc; + } + + private static unsafe void NativeFree(IntPtr mem) + { + NativeMemory.Free(mem.ToPointer()); + } + + public void Dispose() + { + Scanner.Dispose(); + + foreach (var hook in Hooks.Values) + { + hook.Disable(); + hook.Dispose(); + } + + Hooks.Clear(); + + foreach (var mem in NativeAllocList) + { + NativeFree(mem); + } + + NativeAllocList.Clear(); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs new file mode 100644 index 00000000..231e04f3 --- /dev/null +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -0,0 +1,194 @@ +using System.IO.MemoryMappedFiles; +using Iced.Intel; +using PeNet; +using Decoder = Iced.Intel.Decoder; + +namespace Penumbra.Interop.Hooks.ResourceLoading; + +// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause I could not be faffed, maybe I'll rewrite it later +public class PeSigScanner : IDisposable +{ + private MemoryMappedFile File { get; } + + private uint TextSectionStart { get; } + private uint TextSectionSize { get; } + + private IntPtr ModuleBaseAddress { get; } + private uint TextSectionVirtualAddress { get; } + + private MemoryMappedViewAccessor TextSection { get; } + + + public PeSigScanner() + { + var mainModule = Process.GetCurrentProcess().MainModule!; + var fileName = mainModule.FileName; + ModuleBaseAddress = mainModule.BaseAddress; + + if (fileName == null) + { + throw new Exception("Can't get main module path, the fuck is going on?"); + } + + File = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + + using var fileStream = File.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); + var pe = new PeFile(fileStream); + + var textSection = pe.ImageSectionHeaders!.First(header => header.Name == ".text"); + + TextSectionStart = textSection.PointerToRawData; + TextSectionSize = textSection.SizeOfRawData; + TextSectionVirtualAddress = textSection.VirtualAddress; + + TextSection = File.CreateViewAccessor(TextSectionStart, TextSectionSize, MemoryMappedFileAccess.Read); + } + + + private IntPtr ScanText(string signature) + { + var scanRet = Scan(TextSection, signature); + + var instrByte = Marshal.ReadByte(scanRet); + + if (instrByte is 0xE8 or 0xE9) + scanRet = ReadJmpCallSig(scanRet); + + return scanRet; + } + + private static IntPtr ReadJmpCallSig(IntPtr sigLocation) + { + var jumpOffset = Marshal.ReadInt32(sigLocation, 1); + return IntPtr.Add(sigLocation, 5 + jumpOffset); + } + + public bool TryScanText(string signature, out IntPtr result) + { + try + { + result = ScanText(signature); + return true; + } + catch (KeyNotFoundException) + { + result = IntPtr.Zero; + return false; + } + } + + private IntPtr Scan(MemoryMappedViewAccessor section, string signature) + { + var (needle, mask) = ParseSignature(signature); + + var index = IndexOf(section, needle, mask); + if (index < 0) + throw new KeyNotFoundException($"Can't find a signature of {signature}"); + return new IntPtr(ModuleBaseAddress + index - section.PointerOffset + TextSectionVirtualAddress); + } + + private static (byte[] Needle, bool[] Mask) ParseSignature(string signature) + { + signature = signature.Replace(" ", string.Empty); + if (signature.Length % 2 != 0) + throw new ArgumentException("Signature without whitespaces must be divisible by two.", nameof(signature)); + + var needleLength = signature.Length / 2; + var needle = new byte[needleLength]; + var mask = new bool[needleLength]; + for (var i = 0; i < needleLength; i++) + { + var hexString = signature.Substring(i * 2, 2); + if (hexString == "??" || hexString == "**") + { + needle[i] = 0; + mask[i] = true; + continue; + } + + needle[i] = byte.Parse(hexString, NumberStyles.AllowHexSpecifier); + mask[i] = false; + } + + return (needle, mask); + } + + private static unsafe int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask) + { + if (needle.Length > section.Capacity) return -1; + var badShift = BuildBadCharTable(needle, mask); + var last = needle.Length - 1; + var offset = 0; + var maxOffset = section.Capacity - needle.Length; + + byte* buffer = null; + section.SafeMemoryMappedViewHandle.AcquirePointer(ref buffer); + try + { + while (offset <= maxOffset) + { + int position; + for (position = last; needle[position] == *(buffer + position + offset) || mask[position]; position--) + { + if (position == 0) + return offset; + } + + offset += badShift[*(buffer + offset + last)]; + } + } + finally + { + section.SafeMemoryMappedViewHandle.ReleasePointer(); + } + + return -1; + } + + + private static int[] BuildBadCharTable(byte[] needle, bool[] mask) + { + int idx; + var last = needle.Length - 1; + var badShift = new int[256]; + for (idx = last; idx > 0 && !mask[idx]; --idx) + { + } + + var diff = last - idx; + if (diff == 0) diff = 1; + + for (idx = 0; idx <= 255; ++idx) + badShift[idx] = diff; + for (idx = last - diff; idx < last; ++idx) + badShift[needle[idx]] = last - idx; + return badShift; + } + + // Detects function termination; this is done in a really stupid way that will possibly break if looked at wrong, but it'll work for now + // If this shits itself, go bother Winter to implement proper CFG + basic block detection + public IEnumerable GetFunctionInstructions(IntPtr addr) + { + var fileOffset = addr - TextSectionVirtualAddress - ModuleBaseAddress; + + var codeReader = new MappedCodeReader(TextSection, fileOffset); + var decoder = Decoder.Create(64, codeReader, (ulong)addr.ToInt64()); + + do + { + decoder.Decode(out var instr); + + // Yes, this is catastrophically bad, but it works for some cases okay + if (instr.Mnemonic == Mnemonic.Int3) + break; + + yield return instr; + } while (true); + } + + public void Dispose() + { + TextSection.Dispose(); + File.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 195a8b9e..bc28c200 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -16,6 +16,8 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly ResourceService _resources; private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; + + private readonly PapHandler _papHandler; private ResolveData _resolvedData = ResolveData.Invalid; @@ -30,6 +32,29 @@ public unsafe class ResourceLoader : IDisposable, IService _resources.ResourceHandleIncRef += IncRefProtection; _resources.ResourceHandleDecRef += DecRefProtection; _fileReadService.ReadSqPack += ReadSqPackDetour; + + _papHandler = new PapHandler(PapResourceHandler); + _papHandler.Enable(); + } + + private int PapResourceHandler(void* self, byte* path, int length) + { + Utf8GamePath.FromPointer(path, out var gamePath); + + var (resolvedPath, _) = _incMode.Value + ? (null, ResolveData.Invalid) + : _resolvedData.Valid + ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) + : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); + + if (!resolvedPath.HasValue || !Utf8GamePath.FromString(resolvedPath.Value.FullName, out var utf8ResolvedPath)) + { + return length; + } + + NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); + path[utf8ResolvedPath.Length] = 0; + return utf8ResolvedPath.Length; } /// Load a resource for a given path and a specific collection. @@ -84,6 +109,7 @@ public unsafe class ResourceLoader : IDisposable, IService _resources.ResourceHandleIncRef -= IncRefProtection; _resources.ResourceHandleDecRef -= DecRefProtection; _fileReadService.ReadSqPack -= ReadSqPackDetour; + _papHandler.Dispose(); } private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 2e53bd22..70208737 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,10 +23,10 @@ PROFILING; - - - - + + + + @@ -68,6 +68,10 @@ $(DalamudLibPath)Newtonsoft.Json.dll False + + $(DalamudLibPath)Iced.dll + False + lib\OtterTex.dll @@ -79,6 +83,7 @@ + From 8351b74b21c5dfaca5691bc7e2956715573e7fa5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 22:23:31 +0200 Subject: [PATCH 259/865] Update GameData and packages. --- Penumbra.GameData | 2 +- Penumbra/packages.lock.json | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9f1816f1..f13818fd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9f1816f1b75003d01c5576769831c10f3d8948a7 +Subproject commit f13818fd85b436d0a0f66293fe7c6b60d4bffe3c diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index b431e595..42539e78 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -11,6 +11,16 @@ "Unosquare.Swan.Lite": "3.0.0" } }, + "PeNet": { + "type": "Direct", + "requested": "[4.0.5, )", + "resolved": "4.0.5", + "contentHash": "/OUfRnMG8STVuK8kTpdfm+WQGTDoUiJnI845kFw4QrDmv2gYourmSnH84pqVjHT1YHBSuRfCzfioIpHGjFJrGA==", + "dependencies": { + "PeNet.Asn1": "2.0.1", + "System.Security.Cryptography.Pkcs": "8.0.0" + } + }, "SharpCompress": { "type": "Direct", "requested": "[0.33.0, )", @@ -56,6 +66,11 @@ "resolved": "8.0.0", "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, + "PeNet.Asn1": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "YR2O2YokSAYB+7CXkCDN3bd6/p0K3/AicCPkOJHKUz500v1D/hulCuVlggguqNc3M0LgSfOZKGvVYg2ud1GA9A==" + }, "SharpGLTF.Runtime": { "type": "Transitive", "resolved": "1.0.0-alpha0030", @@ -64,6 +79,19 @@ "SharpGLTF.Core": "1.0.0-alpha0030" } }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w==" + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ULmp3xoOwNYjOYp4JZ2NK/6NdTgiN1GQXzVVN1njQ7LOZ0d0B9vyMnhyqbIi9Qw4JXj1JgCsitkTShboHRx7Eg==", + "dependencies": { + "System.Formats.Asn1": "8.0.0" + } + }, "System.ValueTuple": { "type": "Transitive", "resolved": "4.5.0", @@ -94,7 +122,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.0.0, )", + "Penumbra.Api": "[5.2.0, )", "Penumbra.String": "[1.0.4, )" } }, From 0db70c89b105ec8b76a0c3a6764ab4c566e016f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 22:58:03 +0200 Subject: [PATCH 260/865] Some cleanup of PeSigScanner. --- .../Hooks/ResourceLoading/PapHandler.cs | 4 +- .../Hooks/ResourceLoading/PeSigScanner.cs | 118 +++++++++--------- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index 29d77d83..f0fd8b0e 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -17,7 +17,5 @@ public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResour } public void Dispose() - { - _papRewriter.Dispose(); - } + => _papRewriter.Dispose(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs index 231e04f3..f5dd2d45 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -5,65 +5,56 @@ using Decoder = Iced.Intel.Decoder; namespace Penumbra.Interop.Hooks.ResourceLoading; -// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause I could not be faffed, maybe I'll rewrite it later -public class PeSigScanner : IDisposable +// A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause Winter could not be faffed, Winter will definitely not rewrite it later +public unsafe class PeSigScanner : IDisposable { - private MemoryMappedFile File { get; } + private readonly MemoryMappedFile _file; + private readonly MemoryMappedViewAccessor _textSection; + + private readonly nint _moduleBaseAddress; + private readonly uint _textSectionVirtualAddress; + - private uint TextSectionStart { get; } - private uint TextSectionSize { get; } - - private IntPtr ModuleBaseAddress { get; } - private uint TextSectionVirtualAddress { get; } - - private MemoryMappedViewAccessor TextSection { get; } - - public PeSigScanner() { var mainModule = Process.GetCurrentProcess().MainModule!; - var fileName = mainModule.FileName; - ModuleBaseAddress = mainModule.BaseAddress; + var fileName = mainModule.FileName; + _moduleBaseAddress = mainModule.BaseAddress; if (fileName == null) - { - throw new Exception("Can't get main module path, the fuck is going on?"); - } - - File = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + throw new Exception("Unable to obtain main module path. This should not happen."); - using var fileStream = File.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); + _file = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read); + + using var fileStream = _file.CreateViewStream(0, 0, MemoryMappedFileAccess.Read); var pe = new PeFile(fileStream); var textSection = pe.ImageSectionHeaders!.First(header => header.Name == ".text"); - TextSectionStart = textSection.PointerToRawData; - TextSectionSize = textSection.SizeOfRawData; - TextSectionVirtualAddress = textSection.VirtualAddress; + var textSectionStart = textSection.PointerToRawData; + var textSectionSize = textSection.SizeOfRawData; + _textSectionVirtualAddress = textSection.VirtualAddress; - TextSection = File.CreateViewAccessor(TextSectionStart, TextSectionSize, MemoryMappedFileAccess.Read); + _textSection = _file.CreateViewAccessor(textSectionStart, textSectionSize, MemoryMappedFileAccess.Read); } - private IntPtr ScanText(string signature) + private nint ScanText(string signature) { - var scanRet = Scan(TextSection, signature); - - var instrByte = Marshal.ReadByte(scanRet); - - if (instrByte is 0xE8 or 0xE9) + var scanRet = Scan(_textSection, signature); + if (*(byte*)scanRet is 0xE8 or 0xE9) scanRet = ReadJmpCallSig(scanRet); return scanRet; } - - private static IntPtr ReadJmpCallSig(IntPtr sigLocation) + + private static nint ReadJmpCallSig(nint sigLocation) { - var jumpOffset = Marshal.ReadInt32(sigLocation, 1); - return IntPtr.Add(sigLocation, 5 + jumpOffset); + var jumpOffset = *(int*)(sigLocation + 1); + return sigLocation + 5 + jumpOffset; } - - public bool TryScanText(string signature, out IntPtr result) + + public bool TryScanText(string signature, out nint result) { try { @@ -72,21 +63,22 @@ public class PeSigScanner : IDisposable } catch (KeyNotFoundException) { - result = IntPtr.Zero; + result = nint.Zero; return false; } } - - private IntPtr Scan(MemoryMappedViewAccessor section, string signature) + + private nint Scan(MemoryMappedViewAccessor section, string signature) { var (needle, mask) = ParseSignature(signature); - - var index = IndexOf(section, needle, mask); + + var index = IndexOf(section, needle, mask); if (index < 0) throw new KeyNotFoundException($"Can't find a signature of {signature}"); - return new IntPtr(ModuleBaseAddress + index - section.PointerOffset + TextSectionVirtualAddress); + + return (nint)(_moduleBaseAddress + index - section.PointerOffset + _textSectionVirtualAddress); } - + private static (byte[] Needle, bool[] Mask) ParseSignature(string signature) { signature = signature.Replace(" ", string.Empty); @@ -99,7 +91,7 @@ public class PeSigScanner : IDisposable for (var i = 0; i < needleLength; i++) { var hexString = signature.Substring(i * 2, 2); - if (hexString == "??" || hexString == "**") + if (hexString is "??" or "**") { needle[i] = 0; mask[i] = true; @@ -112,10 +104,12 @@ public class PeSigScanner : IDisposable return (needle, mask); } - - private static unsafe int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask) + + private static int IndexOf(MemoryMappedViewAccessor section, byte[] needle, bool[] mask) { - if (needle.Length > section.Capacity) return -1; + if (needle.Length > section.Capacity) + return -1; + var badShift = BuildBadCharTable(needle, mask); var last = needle.Length - 1; var offset = 0; @@ -144,19 +138,19 @@ public class PeSigScanner : IDisposable return -1; } - - + + private static int[] BuildBadCharTable(byte[] needle, bool[] mask) { int idx; var last = needle.Length - 1; var badShift = new int[256]; for (idx = last; idx > 0 && !mask[idx]; --idx) - { - } + { } - var diff = last - idx; - if (diff == 0) diff = 1; + var diff = last - idx; + if (diff == 0) + diff = 1; for (idx = 0; idx <= 255; ++idx) badShift[idx] = diff; @@ -164,16 +158,16 @@ public class PeSigScanner : IDisposable badShift[needle[idx]] = last - idx; return badShift; } - + // Detects function termination; this is done in a really stupid way that will possibly break if looked at wrong, but it'll work for now // If this shits itself, go bother Winter to implement proper CFG + basic block detection - public IEnumerable GetFunctionInstructions(IntPtr addr) + public IEnumerable GetFunctionInstructions(nint address) { - var fileOffset = addr - TextSectionVirtualAddress - ModuleBaseAddress; - - var codeReader = new MappedCodeReader(TextSection, fileOffset); - var decoder = Decoder.Create(64, codeReader, (ulong)addr.ToInt64()); - + var fileOffset = address - _textSectionVirtualAddress - _moduleBaseAddress; + + var codeReader = new MappedCodeReader(_textSection, fileOffset); + var decoder = Decoder.Create(64, codeReader, (ulong)address.ToInt64()); + do { decoder.Decode(out var instr); @@ -188,7 +182,7 @@ public class PeSigScanner : IDisposable public void Dispose() { - TextSection.Dispose(); - File.Dispose(); + _textSection.Dispose(); + _file.Dispose(); } } From ee5a21f7a20dc459b1ec0d903c008f616fbcde3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 22:58:24 +0200 Subject: [PATCH 261/865] Add pap requested event, some cleanup. --- .../Hooks/ResourceLoading/ResourceLoader.cs | 22 +++++---- .../UI/ResourceWatcher/ResourceWatcher.cs | 47 ++++++++++++------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index bc28c200..cf87aa2b 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -16,10 +16,10 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly ResourceService _resources; private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; - - private readonly PapHandler _papHandler; + private readonly PapHandler _papHandler; - private ResolveData _resolvedData = ResolveData.Invalid; + private ResolveData _resolvedData = ResolveData.Invalid; + public event Action? PapRequested; public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { @@ -36,24 +36,28 @@ public unsafe class ResourceLoader : IDisposable, IService _papHandler = new PapHandler(PapResourceHandler); _papHandler.Enable(); } - + private int PapResourceHandler(void* self, byte* path, int length) { - Utf8GamePath.FromPointer(path, out var gamePath); - + if (!Utf8GamePath.FromPointer(path, out var gamePath)) + return length; + var (resolvedPath, _) = _incMode.Value ? (null, ResolveData.Invalid) : _resolvedData.Valid ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); - - if (!resolvedPath.HasValue || !Utf8GamePath.FromString(resolvedPath.Value.FullName, out var utf8ResolvedPath)) + + + if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) { + PapRequested?.Invoke(gamePath, gamePath, _resolvedData); return length; } - + NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); path[utf8ResolvedPath.Length] = 0; + PapRequested?.Invoke(gamePath, utf8ResolvedPath, _resolvedData); return utf8ResolvedPath.Length; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 935f11e3..a00b33c7 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -36,31 +36,47 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService private Regex? _logRegex; private int _newMaxEntries; - public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, ResourceHandleDestructor destructor) + public unsafe ResourceWatcher(ActorManager actors, Configuration config, ResourceService resources, ResourceLoader loader, + ResourceHandleDestructor destructor) { - _actors = actors; - _config = config; - _ephemeral = config.Ephemeral; - _resources = resources; - _destructor = destructor; - _loader = loader; - _table = new ResourceWatcherTable(config.Ephemeral, _records); - _resources.ResourceRequested += OnResourceRequested; + _actors = actors; + _config = config; + _ephemeral = config.Ephemeral; + _resources = resources; + _destructor = destructor; + _loader = loader; + _table = new ResourceWatcherTable(config.Ephemeral, _records); + _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); - _loader.ResourceLoaded += OnResourceLoaded; - _loader.FileLoaded += OnFileLoaded; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.FileLoaded += OnFileLoaded; + _loader.PapRequested += OnPapRequested; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _newMaxEntries = _config.MaxResourceWatcherRecords; } + private void OnPapRequested(Utf8GamePath original) + { + if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) + Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); + + if (!_ephemeral.EnableResourceWatcher) + return; + + var record = Record.CreateRequest(original.Path, false); + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + public unsafe void Dispose() { Clear(); _records.TrimExcess(); - _resources.ResourceRequested -= OnResourceRequested; + _resources.ResourceRequested -= OnResourceRequested; _destructor.Unsubscribe(OnResourceDestroyed); - _loader.ResourceLoaded -= OnResourceLoaded; - _loader.FileLoaded -= OnFileLoaded; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.FileLoaded -= OnFileLoaded; + _loader.PapRequested -= OnPapRequested; } private void Clear() @@ -200,8 +216,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService private unsafe void OnResourceRequested(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, - Utf8GamePath original, - GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) + Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested {(sync ? "synchronously." : "asynchronously.")}"); From ceaa9ca29a05125fd08cd76e664145cac37c7343 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 23:26:09 +0200 Subject: [PATCH 262/865] Some further cleanup. --- .../Hooks/ResourceLoading/PapHandler.cs | 25 +- .../Hooks/ResourceLoading/PapRewriter.cs | 230 ++++++++---------- .../Hooks/ResourceLoading/ResourceLoader.cs | 4 +- 3 files changed, 127 insertions(+), 132 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index f0fd8b0e..65add13c 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -5,17 +5,26 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { private readonly PapRewriter _papRewriter = new(papResourceHandler); - + public void Enable() { - _papRewriter.Rewrite(Sigs.LoadAlwaysResidentMotionPacks); - _papRewriter.Rewrite(Sigs.LoadWeaponDependentResidentMotionPacks); - _papRewriter.Rewrite(Sigs.LoadInitialResidentMotionPacks); - _papRewriter.Rewrite(Sigs.LoadMotionPacks); - _papRewriter.Rewrite(Sigs.LoadMotionPacks2); - _papRewriter.Rewrite(Sigs.LoadMigratoryMotionPack); + ReadOnlySpan signatures = + [ + Sigs.LoadAlwaysResidentMotionPacks, + Sigs.LoadWeaponDependentResidentMotionPacks, + Sigs.LoadInitialResidentMotionPacks, + Sigs.LoadMotionPacks, + Sigs.LoadMotionPacks2, + Sigs.LoadMigratoryMotionPack, + ]; + + var stopwatch = Stopwatch.StartNew(); + foreach (var sig in signatures) + _papRewriter.Rewrite(sig); + Penumbra.Log.Debug( + $"[PapHandler] Rewrote {signatures.Length} .pap functions for inlined GetResourceAsync in {stopwatch.ElapsedMilliseconds} ms."); } - + public void Dispose() => _papRewriter.Dispose(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index cb437d9e..af16d706 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using Iced.Intel; +using OtterGui; using Penumbra.String.Classes; namespace Penumbra.Interop.Hooks.ResourceLoading; @@ -7,175 +8,160 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - - private PeSigScanner Scanner { get; } = new(); - private Dictionary Hooks { get; }= []; - private List NativeAllocList { get; } = []; - private PapResourceHandlerPrototype PapResourceHandler { get; } = papResourceHandler; + + private readonly PeSigScanner _scanner = new(); + private readonly Dictionary _hooks = []; + private readonly List _nativeAllocList = []; public void Rewrite(string sig) { - if (!Scanner.TryScanText(sig, out var addr)) - { - throw new Exception($"Sig is fucked: {sig}"); - } + if (!_scanner.TryScanText(sig, out var address)) + throw new Exception($"Signature [{sig}] could not be found."); - var funcInstructions = Scanner.GetFunctionInstructions(addr).ToList(); - - var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); + var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray(); + var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); foreach (var hookPoint in hookPoints) { - var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength); - var stringLoc = NativeAlloc(Utf8GamePath.MaxGamePathLength); + // We'll need to grab our true hook point; the location where we can change the path at our leisure. + // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. + // Pretty scuffed, this might need a refactoring at some point. + // We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL + var skipIndex = funcInstructions.IndexOf(instr => instr.IP == hookPoint.IP) + 1; + var detourPoint = funcInstructions.Skip(skipIndex) + .First(instr => instr.Mnemonic == Mnemonic.Call); - { - // We'll need to grab our true hook point; the location where we can change the path at our leisure. - // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. - // Pretty scuffed, this might need a refactoring at some point. - // We're doing it by skipping to our hookPoint's address in the list of instructions inside the function; then getting next CALL - var detourPoint = funcInstructions.Skip( - funcInstructions.FindIndex(instr => instr.IP == hookPoint.IP) + 1 - ).First(instr => instr.Mnemonic == Mnemonic.Call); + // We'll also remove all the 'hookPoints' from 'stackAccesses'. + // We're handling the char *path redirection here, so we don't want this to hit the later code + foreach (var hp in hookPoints) + stackAccesses.RemoveAll(instr => instr.IP == hp.IP); - // We'll also remove all the 'hookPoints' from 'stackAccesses'. - // We're handling the char *path redirection here, so we don't want this to hit the later code - foreach (var hp in hookPoints) - { - stackAccesses.RemoveAll(instr => instr.IP == hp.IP); - } + var detourPointer = Marshal.GetFunctionPointerForDelegate(papResourceHandler); + var targetRegister = hookPoint.Op0Register.ToString().ToLower(); + var hookAddress = new IntPtr((long)detourPoint.IP); - var pDetour = Marshal.GetFunctionPointerForDelegate(PapResourceHandler); - var targetRegister = hookPoint.Op0Register.ToString().ToLower(); - var hookAddr = new IntPtr((long)detourPoint.IP); + var caveAllocation = NativeAlloc(16); + var hook = new AsmHook( + hookAddress, + [ + "use64", + $"mov {targetRegister}, 0x{stringAllocation:x8}", // Move our char *path into the relevant register (rdx) - var caveLoc = NativeAlloc(16); - var hook = new AsmHook( - hookAddr, - [ - "use64", - $"mov {targetRegister}, 0x{stringLoc:x8}", // Move our char *path into the relevant register (rdx) - - // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves - // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call - // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh - $"mov r9, 0x{caveLoc:x8}", - "mov [r9], rcx", - "mov [r9+0x8], rdx", - - // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway - $"mov rax, 0x{pDetour:x8}", // Get a pointer to our detour in place - "call rax", // Call detour - - // Do the reverse process and retrieve the stored stuff - $"mov r9, 0x{caveLoc:x8}", - "mov rcx, [r9]", - "mov rdx, [r9+0x8]", - - // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call - "mov r8, rax", - ], "Pap Redirection" - ); + // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves + // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call + // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh + $"mov r9, 0x{caveAllocation:x8}", + "mov [r9], rcx", + "mov [r9+0x8], rdx", - Hooks.Add(hookAddr, hook); - hook.Enable(); - } + // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway + $"mov rax, 0x{detourPointer:x8}", // Get a pointer to our detour in place + "call rax", // Call detour + + // Do the reverse process and retrieve the stored stuff + $"mov r9, 0x{caveAllocation:x8}", + "mov rcx, [r9]", + "mov rdx, [r9+0x8]", + + // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call + "mov r8, rax", + ], "Pap Redirection" + ); + + _hooks.Add(hookAddress, hook); + hook.Enable(); // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' - foreach (var stackAccess in stackAccesses) - { - var hookAddr = new IntPtr((long)stackAccess.IP + stackAccess.Length); - - if (Hooks.ContainsKey(hookAddr)) - { - // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip - continue; - } - - var targetRegister = stackAccess.Op0Register.ToString().ToLower(); - var hook = new AsmHook( - hookAddr, - [ - "use64", - $"mov {targetRegister}, 0x{stringLoc:x8}", - ], "Pap Stack Accesses" - ); - - Hooks.Add(hookAddr, hook); - hook.Enable(); - } + UpdatePathAddresses(stackAccesses, stringAllocation); + } + } + + private void UpdatePathAddresses(IEnumerable stackAccesses, nint stringAllocation) + { + foreach (var stackAccess in stackAccesses) + { + var hookAddress = new IntPtr((long)stackAccess.IP + stackAccess.Length); + + // Hook already exists, means there's reuse of the same stack address across 2 GetResourceAsync; just skip + if (_hooks.ContainsKey(hookAddress)) + continue; + + var targetRegister = stackAccess.Op0Register.ToString().ToLower(); + var hook = new AsmHook( + hookAddress, + [ + "use64", + $"mov {targetRegister}, 0x{stringAllocation:x8}", + ], "Pap Stack Accesses" + ); + + _hooks.Add(hookAddress, hook); + hook.Enable(); } - - - } private static IEnumerable ScanStackAccesses(IEnumerable instructions, Instruction hookPoint) { return instructions.Where(instr => - instr.Code == hookPoint.Code - && instr.Op0Kind == hookPoint.Op0Kind - && instr.Op1Kind == hookPoint.Op1Kind - && instr.MemoryBase == hookPoint.MemoryBase - && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) - .GroupBy(instr => instr.IP) - .Select(grp => grp.First()); + instr.Code == hookPoint.Code + && instr.Op0Kind == hookPoint.Op0Kind + && instr.Op1Kind == hookPoint.Op1Kind + && instr.MemoryBase == hookPoint.MemoryBase + && instr.MemoryDisplacement64 == hookPoint.MemoryDisplacement64) + .GroupBy(instr => instr.IP) + .Select(grp => grp.First()); } // This is utterly fucked and hardcoded, but, again, it works // Might be a neat idea for a more versatile kind of signature though - private static IEnumerable ScanPapHookPoints(List funcInstructions) + private static IEnumerable ScanPapHookPoints(Instruction[] funcInstructions) { - for (var i = 0; i < funcInstructions.Count - 8; i++) + for (var i = 0; i < funcInstructions.Length - 8; i++) { - if (funcInstructions[i .. (i + 8)] is + if (funcInstructions.AsSpan(i, 8) is [ - {Code : Code.Lea_r64_m}, - {Code : Code.Lea_r64_m}, - {Mnemonic: Mnemonic.Call}, - {Code : Code.Lea_r64_m}, - {Mnemonic: Mnemonic.Call}, - {Code : Code.Lea_r64_m}, + { Code : Code.Lea_r64_m }, + { Code : Code.Lea_r64_m }, + { Mnemonic: Mnemonic.Call }, + { Code : Code.Lea_r64_m }, + { Mnemonic: Mnemonic.Call }, + { Code : Code.Lea_r64_m }, .., ] ) - { yield return funcInstructions[i]; - } } } - private unsafe IntPtr NativeAlloc(nuint size) + private unsafe nint NativeAlloc(nuint size) { - var caveLoc = new IntPtr(NativeMemory.Alloc(size)); - NativeAllocList.Add(caveLoc); + var caveLoc = (nint)NativeMemory.Alloc(size); + _nativeAllocList.Add(caveLoc); return caveLoc; } - private static unsafe void NativeFree(IntPtr mem) - { - NativeMemory.Free(mem.ToPointer()); - } + private static unsafe void NativeFree(nint mem) + => NativeMemory.Free((void*)mem); public void Dispose() { - Scanner.Dispose(); - - foreach (var hook in Hooks.Values) + _scanner.Dispose(); + + foreach (var hook in _hooks.Values) { hook.Disable(); hook.Dispose(); } - - Hooks.Clear(); - - foreach (var mem in NativeAllocList) - { - NativeFree(mem); - } - NativeAllocList.Clear(); + _hooks.Clear(); + + foreach (var mem in _nativeAllocList) + NativeFree(mem); + + _nativeAllocList.Clear(); } } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index cf87aa2b..002846fa 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -51,13 +51,13 @@ public unsafe class ResourceLoader : IDisposable, IService if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) { - PapRequested?.Invoke(gamePath, gamePath, _resolvedData); + PapRequested?.Invoke(gamePath); return length; } NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); path[utf8ResolvedPath.Length] = 0; - PapRequested?.Invoke(gamePath, utf8ResolvedPath, _resolvedData); + PapRequested?.Invoke(gamePath); return utf8ResolvedPath.Length; } From cec28a1823fcbb228136705ff98733d70ca71c9e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 21 Jul 2024 23:49:27 +0200 Subject: [PATCH 263/865] Provide actual hook names. --- .../Hooks/ResourceLoading/PapHandler.cs | 18 +++++++++--------- .../Hooks/ResourceLoading/PapRewriter.cs | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index 65add13c..ea12a480 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -8,19 +8,19 @@ public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResour public void Enable() { - ReadOnlySpan signatures = + ReadOnlySpan<(string Sig, string Name)> signatures = [ - Sigs.LoadAlwaysResidentMotionPacks, - Sigs.LoadWeaponDependentResidentMotionPacks, - Sigs.LoadInitialResidentMotionPacks, - Sigs.LoadMotionPacks, - Sigs.LoadMotionPacks2, - Sigs.LoadMigratoryMotionPack, + (Sigs.LoadAlwaysResidentMotionPacks, nameof(Sigs.LoadAlwaysResidentMotionPacks)), + (Sigs.LoadWeaponDependentResidentMotionPacks, nameof(Sigs.LoadWeaponDependentResidentMotionPacks)), + (Sigs.LoadInitialResidentMotionPacks, nameof(Sigs.LoadInitialResidentMotionPacks)), + (Sigs.LoadMotionPacks, nameof(Sigs.LoadMotionPacks)), + (Sigs.LoadMotionPacks2, nameof(Sigs.LoadMotionPacks2)), + (Sigs.LoadMigratoryMotionPack, nameof(Sigs.LoadMigratoryMotionPack)), ]; var stopwatch = Stopwatch.StartNew(); - foreach (var sig in signatures) - _papRewriter.Rewrite(sig); + foreach (var (sig, name) in signatures) + _papRewriter.Rewrite(sig, name); Penumbra.Log.Debug( $"[PapHandler] Rewrote {signatures.Length} .pap functions for inlined GetResourceAsync in {stopwatch.ElapsedMilliseconds} ms."); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index af16d706..5a2b09bf 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -13,10 +13,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou private readonly Dictionary _hooks = []; private readonly List _nativeAllocList = []; - public void Rewrite(string sig) + public void Rewrite(string sig, string name) { if (!_scanner.TryScanText(sig, out var address)) - throw new Exception($"Signature [{sig}] could not be found."); + throw new Exception($"Signature for {name} [{sig}] could not be found."); var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray(); var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); @@ -68,20 +68,20 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call "mov r8, rax", - ], "Pap Redirection" + ], $"{name}.PapRedirection" ); _hooks.Add(hookAddress, hook); hook.Enable(); // Now we're adjusting every single reference to the stack allocated 'path' to our substantially bigger 'stringLoc' - UpdatePathAddresses(stackAccesses, stringAllocation); + UpdatePathAddresses(stackAccesses, stringAllocation, name); } } - private void UpdatePathAddresses(IEnumerable stackAccesses, nint stringAllocation) + private void UpdatePathAddresses(IEnumerable stackAccesses, nint stringAllocation, string name) { - foreach (var stackAccess in stackAccesses) + foreach (var (stackAccess, index) in stackAccesses.WithIndex()) { var hookAddress = new IntPtr((long)stackAccess.IP + stackAccess.Length); @@ -95,7 +95,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou [ "use64", $"mov {targetRegister}, 0x{stringAllocation:x8}", - ], "Pap Stack Accesses" + ], $"{name}.PapStackAccess[{index}]" ); _hooks.Add(hookAddress, hook); From 1501bd4fbf5b05be2b770b0e1b8b856d71f1dce3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jul 2024 12:41:20 +0200 Subject: [PATCH 264/865] Fix negative matching on folders with no matches. --- Penumbra/UI/ModsTab/ModSearchStringSplitter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs index 1ea70731..e7550eea 100644 --- a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -95,9 +95,9 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter MatchesName(i, folder.Name, fullName)) - && !Negated.Any(i => MatchesName(i, folder.Name, fullName)) - && (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName))); + return Forced.All(i => MatchesName(i, folder.Name, fullName, false)) + && !Negated.Any(i => MatchesName(i, folder.Name, fullName, true)) + && (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName, false))); } protected override bool Matches(Entry entry, ModFileSystem.Leaf leaf) @@ -128,11 +128,11 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter true, }; - private static bool MatchesName(Entry entry, ReadOnlySpan name, ReadOnlySpan fullName) + private static bool MatchesName(Entry entry, ReadOnlySpan name, ReadOnlySpan fullName, bool defaultValue) => entry.Type switch { ModSearchType.Default => fullName.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), ModSearchType.Name => name.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase), - _ => false, + _ => defaultValue, }; } From 29dce8f3ab3656253a6c843edde527d2442f9199 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 22 Jul 2024 13:16:22 +0000 Subject: [PATCH 265/865] [CI] Updating repo.json for testing_1.2.0.13 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ca13bb5b..b6d37a92 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.12", + "TestingAssemblyVersion": "1.2.0.13", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.13/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ca648f98a173fa75fc538e0a1c1bc0630b64ac55 Mon Sep 17 00:00:00 2001 From: pmgr <26606291+pmgr@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:37:57 +0100 Subject: [PATCH 266/865] Fix for pap weirdness, hopefully --- .../Hooks/ResourceLoading/PapRewriter.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 5a2b09bf..84cd0c11 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -2,6 +2,7 @@ using Dalamud.Hooking; using Iced.Intel; using OtterGui; using Penumbra.String.Classes; +using Swan; namespace Penumbra.Interop.Hooks.ResourceLoading; @@ -9,9 +10,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - private readonly PeSigScanner _scanner = new(); - private readonly Dictionary _hooks = []; - private readonly List _nativeAllocList = []; + private readonly PeSigScanner _scanner = new(); + private readonly Dictionary _hooks = []; + private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; + private readonly List _nativeAllocCaves = []; public void Rewrite(string sig, string name) { @@ -24,7 +26,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou foreach (var hookPoint in hookPoints) { var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); - var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength); + var stringAllocation = NativeAllocPath( + address, hookPoint.MemoryBase, hookPoint.MemoryDisplacement64, + Utf8GamePath.MaxGamePathLength + ); // We'll need to grab our true hook point; the location where we can change the path at our leisure. // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. @@ -43,7 +48,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou var targetRegister = hookPoint.Op0Register.ToString().ToLower(); var hookAddress = new IntPtr((long)detourPoint.IP); - var caveAllocation = NativeAlloc(16); + var caveAllocation = NativeAllocCave(16); var hook = new AsmHook( hookAddress, [ @@ -136,13 +141,24 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou } } - private unsafe nint NativeAlloc(nuint size) + private unsafe nint NativeAllocCave(nuint size) { var caveLoc = (nint)NativeMemory.Alloc(size); - _nativeAllocList.Add(caveLoc); + _nativeAllocCaves.Add(caveLoc); return caveLoc; } + + // This is a bit conked but, if we identify a path by: + // 1) The function it belongs to (starting address, 'funcAddress') + // 2) The stack register (not strictly necessary - should always be rbp - but abundance of caution, so I don't hit myself in the future) + // 3) The displacement on the stack + // Then we ensure we have a unique identifier for the specific variable location of that specific function + // This is useful because sometimes the stack address is reused within the same function for different GetResourceAsync calls + private unsafe nint NativeAllocPath(nint funcAddress, Register stackRegister, ulong stackDisplacement, nuint size) + { + return _nativeAllocPaths.GetOrAdd((funcAddress, stackRegister, stackDisplacement), _ => (nint)NativeMemory.Alloc(size)); + } private static unsafe void NativeFree(nint mem) => NativeMemory.Free((void*)mem); @@ -159,9 +175,14 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou _hooks.Clear(); - foreach (var mem in _nativeAllocList) + foreach (var mem in _nativeAllocCaves) NativeFree(mem); - _nativeAllocList.Clear(); + _nativeAllocCaves.Clear(); + + foreach (var mem in _nativeAllocPaths.Values) + NativeFree(mem); + + _nativeAllocPaths.Clear(); } } From a4cd5695fb3e4908e7e51c19e2d42551acc27d19 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jul 2024 20:41:57 +0200 Subject: [PATCH 267/865] Fix some stuff. --- Penumbra/Api/Api/GameStateApi.cs | 23 +++++++++++++++---- .../Animation/ApricotListenerSoundPlay.cs | 2 +- .../Hooks/ResourceLoading/PapRewriter.cs | 10 +++++++- .../Hooks/ResourceLoading/ResourceLoader.cs | 18 +++++++-------- .../UI/ResourceWatcher/ResourceWatcher.cs | 3 +-- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index b035c886..c2cae32b 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -25,12 +25,14 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable _cutsceneService = cutsceneService; _resourceLoader = resourceLoader; _resourceLoader.ResourceLoaded += OnResourceLoaded; + _resourceLoader.PapRequested += OnPapRequested; _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); } public unsafe void Dispose() { _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _resourceLoader.PapRequested -= OnPapRequested; _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); } @@ -67,14 +69,27 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) - ? PenumbraApiEc.Success + ? PenumbraApiEc.Success : PenumbraApiEc.InvalidArgument; private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) { - if (resolveData.AssociatedGameObject != nint.Zero) - GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), - manipulatedPath?.ToString() ?? originalPath.ToString()); + if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null) + { + var original = originalPath.ToString(); + GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original, + manipulatedPath?.ToString() ?? original); + } + } + + private void OnPapRequested(Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData) + { + if (resolveData.AssociatedGameObject != nint.Zero && GameObjectResourceResolved != null) + { + var original = originalPath.ToString(); + GameObjectResourceResolved.Invoke(resolveData.AssociatedGameObject, original, + manipulatedPath?.ToString() ?? original); + } } private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 361fcd4e..96a51027 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -42,7 +42,7 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook> 13) & 1) == 0) return Task.Result.Original(a1, unused, timeOffset); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}."); // Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay) var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270); diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 5a2b09bf..33b124c8 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,3 +1,4 @@ +using System.Text.Unicode; using Dalamud.Hooking; using Iced.Intel; using OtterGui; @@ -25,7 +26,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou { var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); var stringAllocation = NativeAlloc(Utf8GamePath.MaxGamePathLength); - + WriteToAlloc(stringAllocation, Utf8GamePath.MaxGamePathLength, name); // We'll need to grab our true hook point; the location where we can change the path at our leisure. // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. // Pretty scuffed, this might need a refactoring at some point. @@ -164,4 +165,11 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou _nativeAllocList.Clear(); } + + [Conditional("DEBUG")] + private static unsafe void WriteToAlloc(nint alloc, int size, string name) + { + var span = new Span((void*)alloc, size); + Utf8.TryWrite(span, $"Penumbra.{name}\0", out _); + } } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 002846fa..10821287 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -18,8 +18,8 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly TexMdlService _texMdlService; private readonly PapHandler _papHandler; - private ResolveData _resolvedData = ResolveData.Invalid; - public event Action? PapRequested; + private ResolveData _resolvedData = ResolveData.Invalid; + public event Action? PapRequested; public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) { @@ -42,23 +42,23 @@ public unsafe class ResourceLoader : IDisposable, IService if (!Utf8GamePath.FromPointer(path, out var gamePath)) return length; - var (resolvedPath, _) = _incMode.Value + var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) : _resolvedData.Valid ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); - if (!resolvedPath.HasValue || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var utf8ResolvedPath)) + if (!resolvedPath.HasValue) { - PapRequested?.Invoke(gamePath); + PapRequested?.Invoke(gamePath, null, data); return length; } - NativeMemory.Copy(utf8ResolvedPath.Path.Path, path, (nuint)utf8ResolvedPath.Length); - path[utf8ResolvedPath.Length] = 0; - PapRequested?.Invoke(gamePath); - return utf8ResolvedPath.Length; + PapRequested?.Invoke(gamePath, resolvedPath.Value, data); + NativeMemory.Copy(resolvedPath.Value.InternalName.Path, path, (nuint)resolvedPath.Value.InternalName.Length); + path[resolvedPath.Value.InternalName.Length] = 0; + return resolvedPath.Value.InternalName.Length; } /// Load a resource for a given path and a specific collection. diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index a00b33c7..14d69489 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -11,7 +11,6 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; -using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -55,7 +54,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newMaxEntries = _config.MaxResourceWatcherRecords; } - private void OnPapRequested(Utf8GamePath original) + private void OnPapRequested(Utf8GamePath original, FullPath? _1, ResolveData _2) { if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); From df335574778cfee6fa44a5d63a2ef869c5706c11 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 22 Jul 2024 20:54:24 +0200 Subject: [PATCH 268/865] Cleanup. --- Penumbra.Api | 2 +- .../Hooks/ResourceLoading/PapRewriter.cs | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index f4c6144c..86249598 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit f4c6144ca2012b279e6d8aa52b2bef6cc2ba32d9 +Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1 diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index afe00b8d..2fb1623d 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -11,10 +11,10 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - private readonly PeSigScanner _scanner = new(); - private readonly Dictionary _hooks = []; - private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; - private readonly List _nativeAllocCaves = []; + private readonly PeSigScanner _scanner = new(); + private readonly Dictionary _hooks = []; + private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; + private readonly List _nativeAllocCaves = []; public void Rewrite(string sig, string name) { @@ -26,13 +26,13 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou foreach (var hookPoint in hookPoints) { - var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); + var stackAccesses = ScanStackAccesses(funcInstructions, hookPoint).ToList(); var stringAllocation = NativeAllocPath( address, hookPoint.MemoryBase, hookPoint.MemoryDisplacement64, Utf8GamePath.MaxGamePathLength - ); + ); WriteToAlloc(stringAllocation, Utf8GamePath.MaxGamePathLength, name); - + // We'll need to grab our true hook point; the location where we can change the path at our leisure. // This is going to be the first call instruction after our 'hookPoint', so, we'll find that. // Pretty scuffed, this might need a refactoring at some point. @@ -150,7 +150,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou return caveLoc; } - + // This is a bit conked but, if we identify a path by: // 1) The function it belongs to (starting address, 'funcAddress') // 2) The stack register (not strictly necessary - should always be rbp - but abundance of caution, so I don't hit myself in the future) @@ -158,9 +158,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou // Then we ensure we have a unique identifier for the specific variable location of that specific function // This is useful because sometimes the stack address is reused within the same function for different GetResourceAsync calls private unsafe nint NativeAllocPath(nint funcAddress, Register stackRegister, ulong stackDisplacement, nuint size) - { - return _nativeAllocPaths.GetOrAdd((funcAddress, stackRegister, stackDisplacement), _ => (nint)NativeMemory.Alloc(size)); - } + => _nativeAllocPaths.GetOrAdd((funcAddress, stackRegister, stackDisplacement), _ => (nint)NativeMemory.Alloc(size)); private static unsafe void NativeFree(nint mem) => NativeMemory.Free((void*)mem); @@ -181,7 +179,7 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou NativeFree(mem); _nativeAllocCaves.Clear(); - + foreach (var mem in _nativeAllocPaths.Values) NativeFree(mem); From 30f92338627b2d29558a96aa66eb6e1184443138 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 22 Jul 2024 18:56:35 +0000 Subject: [PATCH 269/865] [CI] Updating repo.json for testing_1.2.0.14 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index b6d37a92..f1108cfe 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.13", + "TestingAssemblyVersion": "1.2.0.14", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.13/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.14/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c501d0b36524d163a45758901390cb840f7f1107 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Jul 2024 15:11:35 +0200 Subject: [PATCH 270/865] Fix card actor identification. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f13818fd..f5a74c70 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f13818fd85b436d0a0f66293fe7c6b60d4bffe3c +Subproject commit f5a74c70ad3861c5c66e1df6ae9a29fc7a0d736a From 72f2834dfd13352f70f10e77b1d40a2910198f13 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Jul 2024 15:11:52 +0200 Subject: [PATCH 271/865] Add some resource flags. --- Penumbra/Enums/ResourceTypeFlag.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Penumbra/Enums/ResourceTypeFlag.cs b/Penumbra/Enums/ResourceTypeFlag.cs index 0cfc5469..461e7ac1 100644 --- a/Penumbra/Enums/ResourceTypeFlag.cs +++ b/Penumbra/Enums/ResourceTypeFlag.cs @@ -60,6 +60,13 @@ public enum ResourceTypeFlag : ulong Uld = 0x0002_0000_0000_0000, Waoe = 0x0004_0000_0000_0000, Wtd = 0x0008_0000_0000_0000, + Bklb = 0x0010_0000_0000_0000, + Cutb = 0x0020_0000_0000_0000, + Eanb = 0x0040_0000_0000_0000, + Eslb = 0x0080_0000_0000_0000, + Fpeb = 0x0100_0000_0000_0000, + Kdb = 0x0200_0000_0000_0000, + Kdlb = 0x0400_0000_0000_0000, } [Flags] @@ -141,6 +148,13 @@ public static class ResourceExtensions ResourceType.Uld => ResourceTypeFlag.Uld, ResourceType.Waoe => ResourceTypeFlag.Waoe, ResourceType.Wtd => ResourceTypeFlag.Wtd, + ResourceType.Bklb => ResourceTypeFlag.Bklb, + ResourceType.Cutb => ResourceTypeFlag.Cutb, + ResourceType.Eanb => ResourceTypeFlag.Eanb, + ResourceType.Eslb => ResourceTypeFlag.Eslb, + ResourceType.Fpeb => ResourceTypeFlag.Fpeb, + ResourceType.Kdb => ResourceTypeFlag.Kdb , + ResourceType.Kdlb => ResourceTypeFlag.Kdlb, _ => 0, }; @@ -148,7 +162,7 @@ public static class ResourceExtensions => (type.ToFlag() & flags) != 0; public static ResourceCategoryFlag ToFlag(this ResourceCategory type) - => type switch + => (ResourceCategory)((uint) type & 0x00FFFFFF) switch { ResourceCategory.Common => ResourceCategoryFlag.Common, ResourceCategory.BgCommon => ResourceCategoryFlag.BgCommon, From 246f4f65f5bf60a9c1ee8ecc7bc65075346c6229 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 26 Jul 2024 18:42:48 +0200 Subject: [PATCH 272/865] Make items changed in a mod sort before other items for item swap, also color them. --- OtterGui | 2 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 10 ++-- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 65 ++++++++++++++--------- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/OtterGui b/OtterGui index dc17161b..87a53262 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit dc17161b1d9c47ffd6bcc17e91f4832cf7762993 +Subproject commit 87a532620d622a00e60059b5dd42a04f0319b5b5 diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 1f4c5e7a..03abfc45 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -15,13 +15,9 @@ public static class ItemSwap public class InvalidItemTypeException : Exception { } - public class MissingFileException : Exception + public class MissingFileException(ResourceType type, object path) : Exception($"Could not load {type} File Data for \"{path}\".") { - public readonly ResourceType Type; - - public MissingFileException(ResourceType type, object path) - : base($"Could not load {type} File Data for \"{path}\".") - => Type = type; + public readonly ResourceType Type = type; } private static bool LoadFile(MetaFileManager manager, FullPath path, out byte[] data) @@ -47,7 +43,7 @@ public static class ItemSwap Penumbra.Log.Debug($"Could not load file {path}:\n{e}"); } - data = Array.Empty(); + data = []; return false; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6db4db5c..da2daeb7 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -22,6 +22,7 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; namespace Penumbra.UI.AdvancedWindow; @@ -34,7 +35,8 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private readonly MetaFileManager _metaFileManager; public ItemSwapTab(CommunicatorService communicator, ItemData itemService, CollectionManager collectionManager, - ModManager modManager, ObjectIdentification identifier, MetaFileManager metaFileManager, Configuration config) + ModManager modManager, ModFileSystemSelector selector, ObjectIdentification identifier, MetaFileManager metaFileManager, + Configuration config) { _communicator = communicator; _collectionManager = collectionManager; @@ -46,15 +48,15 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, FullEquipType.Head), new ItemSelector(itemService, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, FullEquipType.Body), new ItemSelector(itemService, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, FullEquipType.Hands), new ItemSelector(itemService, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, FullEquipType.Legs), new ItemSelector(itemService, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, FullEquipType.Feet), new ItemSelector(itemService, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, FullEquipType.Ears), new ItemSelector(itemService, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, FullEquipType.Neck), new ItemSelector(itemService, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, FullEquipType.Wrists), new ItemSelector(itemService, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, FullEquipType.Finger), new ItemSelector(itemService, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), // @formatter:on }; @@ -129,11 +131,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Weapon, } - private class ItemSelector(ItemData data, FullEquipType type) - : FilterComboCache(() => data.ByType[type], MouseWheelType.None, Penumbra.Log) + private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) + : FilterComboCache<(EquipItem Item, bool InMod)>(() => + { + var list = data.ByType[type]; + if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type)) + return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); + + return list.Select(i => (i, false)).ToList(); + }, MouseWheelType.None, Penumbra.Log) { - protected override string ToString(EquipItem obj) - => obj.Name; + protected override bool DrawSelectable(int globalIdx, bool selected) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value(), Items[globalIdx].InMod); + return base.DrawSelectable(globalIdx, selected); + } + + protected override string ToString((EquipItem Item, bool InMod) obj) + => obj.Item.Name; } private readonly Dictionary _selectors; @@ -186,17 +201,17 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Bracelet: case SwapType.Ring: var values = _selectors[_lastTab]; - if (values.Source.CurrentSelection.Type != FullEquipType.Unknown - && values.Target.CurrentSelection.Type != FullEquipType.Unknown) - _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection, + if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown + && values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) + _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); - if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection, _slotFrom, selectorFrom.CurrentSelection, + if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) + _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item, _slotFrom, selectorFrom.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -455,7 +470,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -480,7 +495,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (_affectedItems is not { Length: > 1 }) return; @@ -489,7 +504,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name)) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)) .Select(i => i.Name))); } @@ -521,7 +536,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text1); ImGui.TableNextColumn(); - _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) @@ -534,7 +549,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(text2); ImGui.TableNextColumn(); - _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); if (type == SwapType.Ring) { @@ -549,7 +564,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name)) + ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)) .Select(i => i.Name))); } From 3ffe6151ffd05b732ac9df38cac17ba70b3a68ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:08:41 +0200 Subject: [PATCH 273/865] Add ToString for InternalEqpEntry. --- Penumbra/Meta/Manipulations/Eqp.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index ec4dd6e7..5d37aac8 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -72,4 +72,7 @@ public readonly record struct EqpEntryInternal(uint Value) var (offset, mask) = Eqp.OffsetAndMask(slot); return (uint)((ulong)(entry & mask) >> offset); } + + public override string ToString() + => Value.ToString("X8"); } From f143601aa00d8807fdf6fb016262afdc57cb00ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:08:58 +0200 Subject: [PATCH 274/865] Do not replace paths when mods are not enabled. --- Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 10821287..3f055f64 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -17,15 +17,17 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly FileReadService _fileReadService; private readonly TexMdlService _texMdlService; private readonly PapHandler _papHandler; + private readonly Configuration _config; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, Configuration config) { _resources = resources; _fileReadService = fileReadService; _texMdlService = texMdlService; + _config = config; ResetResolvePath(); _resources.ResourceRequested += ResourceHandler; @@ -39,7 +41,7 @@ public unsafe class ResourceLoader : IDisposable, IService private int PapResourceHandler(void* self, byte* path, int length) { - if (!Utf8GamePath.FromPointer(path, out var gamePath)) + if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, out var gamePath)) return length; var (resolvedPath, data) = _incMode.Value @@ -119,7 +121,7 @@ public unsafe class ResourceLoader : IDisposable, IService private void ResourceHandler(ref ResourceCategory category, ref ResourceType type, ref int hash, ref Utf8GamePath path, Utf8GamePath original, GetResourceParameters* parameters, ref bool sync, ref ResourceHandle* returnValue) { - if (returnValue != null) + if (!_config.EnableMods || returnValue != null) return; CompareHash(ComputeHash(path.Path, parameters), hash, path); From 6f3d9eb272b7ad35fade8c951df8254285bb7fa5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:09:11 +0200 Subject: [PATCH 275/865] Fix bug in EquipmentSwap --- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index b7827c47..2c292a14 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -186,7 +186,7 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, From cbf5baf65cef7450bbcb2be667d6fe06f69cb7f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:09:48 +0200 Subject: [PATCH 276/865] Remove Material Reassignment Tab from advanced editing due to being obsolete. --- .../AdvancedWindow/ModEditWindow.Materials.cs | 72 ++----------------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 - 2 files changed, 7 insertions(+), 66 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 0223ca6b..c3483c35 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -112,13 +111,13 @@ public partial class ModEditWindow } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) - { - ImGui.AlignTextToFramePadding(); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); + using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); } if (unfolded) @@ -189,61 +188,4 @@ public partial class ModEditWindow if (t) Widget.DrawHexViewer(file.AdditionalData); } - - private void DrawMaterialReassignmentTab() - { - if (_editor.Files.Mdl.Count == 0) - return; - - using var tab = ImRaii.TabItem("Material Reassignment"); - if (!tab) - return; - - ImGui.NewLine(); - MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); - - ImGui.NewLine(); - using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); - if (!child) - return; - - using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); - if (!table) - return; - - var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) - { - using var id = ImRaii.PushId(idx); - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) - info.Save(_editor.Compactor); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true)) - info.Restore(); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(400 * UiHelpers.Scale); - var tmp = info.CurrentMaterials[0]; - if (ImGui.InputText("##0", ref tmp, 64)) - info.SetMaterial(tmp, 0); - - for (var i = 1; i < info.Count; ++i) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(400 * UiHelpers.Scale); - tmp = info.CurrentMaterials[i]; - if (ImGui.InputText($"##{i}", ref tmp, 64)) - info.SetMaterial(tmp, i); - } - } - } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e915a879..79a8ae34 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -179,7 +179,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService DrawSwapTab(); _modMergeTab.Draw(); DrawDuplicatesTab(); - DrawMaterialReassignmentTab(); DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); From a1a7487897aeb8980e82db90b457873c95ae1f49 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:11:20 +0200 Subject: [PATCH 277/865] Remove Update Bibo Button. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index f81b2831..f8874f89 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -111,27 +111,6 @@ public class ModPanelEditTab( MoveDirectory.Draw(modManager, _mod, buttonSize); UiHelpers.DefaultLineSpace(); - DrawUpdateBibo(buttonSize); - - UiHelpers.DefaultLineSpace(); - } - - private void DrawUpdateBibo(Vector2 buttonSize) - { - if (ImGui.Button("Update Bibo Material", buttonSize)) - { - editor.LoadMod(_mod); - editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); - editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); - editor.MdlMaterialEditor.SaveAllModels(editor.Compactor); - editWindow.UpdateModels(); - } - - ImGuiUtil.HoverTooltip( - "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" - + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" - + "Use this for outdated mods made for old Bibo bodies.\n" - + "Go to Advanced Editing for more fine-tuned control over material assignment."); } private void BackupButtons(Vector2 buttonSize) From 19166d8cf4951dafe62766832f7bba00ea25971c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 01:11:52 +0200 Subject: [PATCH 278/865] Add rudimentary start for knowledge window. --- Penumbra/CommandHandler.cs | 20 ++++++--- Penumbra/UI/Knowledge/IKnowledgeTab.cs | 8 ++++ Penumbra/UI/Knowledge/KnowledgeWindow.cs | 55 ++++++++++++++++++++++++ Penumbra/UI/Knowledge/RaceCodeTab.cs | 42 ++++++++++++++++++ Penumbra/UI/WindowSystem.cs | 17 +++++--- 5 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 Penumbra/UI/Knowledge/IKnowledgeTab.cs create mode 100644 Penumbra/UI/Knowledge/KnowledgeWindow.cs create mode 100644 Penumbra/UI/Knowledge/RaceCodeTab.cs diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 484dd954..db8d9aca 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -12,6 +12,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI; +using Penumbra.UI.Knowledge; namespace Penumbra; @@ -29,11 +30,12 @@ public class CommandHandler : IDisposable, IApiService private readonly CollectionManager _collectionManager; private readonly Penumbra _penumbra; private readonly CollectionEditor _collectionEditor; + private readonly KnowledgeWindow _knowledgeWindow; public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, - Configuration config, - ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra, - CollectionEditor collectionEditor) + Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, + Penumbra penumbra, + CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow) { _commandManager = commandManager; _redrawService = redrawService; @@ -45,6 +47,7 @@ public class CommandHandler : IDisposable, IApiService _chat = chat; _penumbra = penumbra; _collectionEditor = collectionEditor; + _knowledgeWindow = knowledgeWindow; framework.RunOnFrameworkThread(() => { if (_commandManager.Commands.ContainsKey(CommandName)) @@ -69,7 +72,7 @@ public class CommandHandler : IDisposable, IApiService var argumentList = arguments.Split(' ', 2); arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty; - var _ = argumentList[0].ToLowerInvariant() switch + _ = argumentList[0].ToLowerInvariant() switch { "window" => ToggleWindow(arguments), "enable" => SetPenumbraState(arguments, true), @@ -83,6 +86,7 @@ public class CommandHandler : IDisposable, IApiService "collection" => SetCollection(arguments), "mod" => SetMod(arguments), "bulktag" => SetTag(arguments), + "knowledge" => HandleKnowledge(arguments), _ => PrintHelp(argumentList[0]), }; } @@ -304,7 +308,7 @@ public class CommandHandler : IDisposable, IApiService identifiers = _actors.FromUserString(split[2], false); } } - catch (ActorManager.IdentifierParseError e) + catch (ActorIdentifierFactory.IdentifierParseError e) { _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true) .AddText($" could not be converted to an identifier. {e.Message}") @@ -619,4 +623,10 @@ public class CommandHandler : IDisposable, IApiService if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text()); } + + private bool HandleKnowledge(string arguments) + { + _knowledgeWindow.Toggle(); + return true; + } } diff --git a/Penumbra/UI/Knowledge/IKnowledgeTab.cs b/Penumbra/UI/Knowledge/IKnowledgeTab.cs new file mode 100644 index 00000000..568d5fda --- /dev/null +++ b/Penumbra/UI/Knowledge/IKnowledgeTab.cs @@ -0,0 +1,8 @@ +namespace Penumbra.UI.Knowledge; + +public interface IKnowledgeTab +{ + public ReadOnlySpan Name { get; } + public ReadOnlySpan SearchTags { get; } + public void Draw(); +} diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs new file mode 100644 index 00000000..de1b36b8 --- /dev/null +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -0,0 +1,55 @@ +using System.Text.Unicode; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Memory; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; + +namespace Penumbra.UI.Knowledge; + +/// Draw the progress information for import. +public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUiService +{ + private readonly IReadOnlyList _tabs = + [ + new RaceCodeTab(), + ]; + + private IKnowledgeTab? _selected; + private readonly byte[] _filterStore = new byte[256]; + + private TerminatedByteString _filter = TerminatedByteString.Empty; + + public override void Draw() + { + DrawSelector(); + ImUtf8.SameLineInner(); + DrawMain(); + } + + private void DrawSelector() + { + using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(250 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); + if (!child) + return; + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + ImUtf8.InputText("##Filter"u8, _filterStore, out _filter, "Filter..."u8); + + foreach (var tab in _tabs) + { + if (ImUtf8.Selectable(tab.Name, _selected == tab)) + _selected = tab; + } + } + + private void DrawMain() + { + using var child = ImUtf8.Child("KnowledgeMain"u8, ImGui.GetContentRegionAvail(), true); + if (!child || _selected == null) + return; + + _selected.Draw(); + } +} diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs new file mode 100644 index 00000000..988506dd --- /dev/null +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -0,0 +1,42 @@ +using ImGuiNET; +using OtterGui.Text; +using Penumbra.GameData.Enums; + +namespace Penumbra.UI.Knowledge; + +public sealed class RaceCodeTab : IKnowledgeTab +{ + public ReadOnlySpan Name + => "Race Codes"u8; + + public ReadOnlySpan SearchTags + => "deformersracecodesmodel"u8; + + public void Draw() + { + using var table = ImUtf8.Table("table"u8, 4, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableHeader("Race Code"u8); + ImUtf8.TableHeader("Race"u8); + ImUtf8.TableHeader("Gender"u8); + ImUtf8.TableHeader("NPC"u8); + + foreach (var genderRace in Enum.GetValues()) + { + ImGui.TableNextColumn(); + ImUtf8.Text(genderRace.ToRaceCode()); + + var (gender, race) = genderRace.Split(); + ImGui.TableNextColumn(); + ImUtf8.Text($"{race}"); + + ImGui.TableNextColumn(); + ImUtf8.Text($"{gender}"); + + ImGui.TableNextColumn(); + ImUtf8.Text(((ushort)genderRace & 0xF) != 1 ? "NPC"u8 : "Normal"u8); + } + } +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 72ac0d01..6d382ad4 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin; using OtterGui.Services; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.Knowledge; using Penumbra.UI.Tabs.Debug; namespace Penumbra.UI; @@ -14,20 +15,24 @@ public class PenumbraWindowSystem : IDisposable, IUiService private readonly FileDialogService _fileDialog; public readonly ConfigWindow Window; public readonly PenumbraChangelog Changelog; + public readonly KnowledgeWindow KnowledgeWindow; public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, - LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab) + LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab, + KnowledgeWindow knowledgeWindow) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + KnowledgeWindow = knowledgeWindow; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); _windowSystem.AddWindow(importPopup); _windowSystem.AddWindow(debugTab); + _windowSystem.AddWindow(KnowledgeWindow); _uiBuilder.OpenMainUi += Window.Toggle; _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; From d0c4d6984cbd8e40db28e47398a309b71382f6ab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 12:57:59 +0200 Subject: [PATCH 279/865] Dispose collection caches on plugin disposal. --- Penumbra/Collections/Cache/CollectionCacheManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 80d4cf1d..a3b6bb83 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -84,6 +84,12 @@ public class CollectionCacheManager : IDisposable, IService _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; + + foreach (var collection in _storage) + { + collection._cache?.Dispose(); + collection._cache = null; + } } public void AddChange(CollectionCache.ChangeData data) From bb4665c367a876d7bff704794651bd673d9c2978 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 12:58:17 +0200 Subject: [PATCH 280/865] Remove unused params. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index f8874f89..90d8fb74 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -11,7 +11,6 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Settings; using Penumbra.UI.ModsTab.Groups; @@ -22,8 +21,6 @@ public class ModPanelEditTab( ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, - ModEditWindow editWindow, - ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config, From 4806f8dc3eeea497ce95f52b30ce7b12bf12dc82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 13:43:01 +0200 Subject: [PATCH 281/865] Do not force loaded game paths to lowercase. --- Penumbra/Mods/SubMods/SubMod.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a8c37369..f6b1be96 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -52,7 +52,7 @@ public static class SubMod if (files != null) foreach (var property in files.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } @@ -60,7 +60,7 @@ public static class SubMod if (swaps != null) foreach (var property in swaps.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } From af0bbeb8bfd0c093716f110e77442a60b69543f9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 13:48:11 +0200 Subject: [PATCH 282/865] Force saving to be synchronous. --- Penumbra/Mods/TemporaryMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e1cf9f2b..e4049482 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -96,7 +96,7 @@ public class TemporaryMod : IMod var manips = new MetaDictionary(collection.MetaCache); defaultMod.Manipulations.UnionWith(manips); - saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); From 5d50523c72ca2ccdb41392b285711315e90ea998 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 28 Jul 2024 12:12:56 +0000 Subject: [PATCH 283/865] [CI] Updating repo.json for testing_1.2.0.15 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f1108cfe..7bcc18b6 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.14", + "TestingAssemblyVersion": "1.2.0.15", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.14/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.15/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From d30d418afe571b8bde490f1566ee421955757c10 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Jul 2024 23:56:11 +0200 Subject: [PATCH 284/865] Remove a ToLower when resolving paths. --- Penumbra/Interop/PathResolving/PathResolver.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 49035dc8..67ec4fc3 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,7 +52,6 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); - path = path.ToLower(); return category switch { // Only Interface collection. From e52b027545e01786dfce4754fe345fabcdae91b0 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 28 Jul 2024 22:03:19 +0000 Subject: [PATCH 285/865] [CI] Updating repo.json for testing_1.2.0.16 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7bcc18b6..9c9e41db 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.15", + "TestingAssemblyVersion": "1.2.0.16", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.15/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.16/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 3754f5713224024aa778456d3ecc7d39b5b483a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 09:27:00 +0200 Subject: [PATCH 286/865] Reinstate spacebar heating --- .../AdvancedWindow/ModEditWindow.Materials.cs | 58 +++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + 2 files changed, 59 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index c3483c35..5a8fb13a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; @@ -188,4 +189,61 @@ public partial class ModEditWindow if (t) Widget.DrawHexViewer(file.AdditionalData); } + + private void DrawMaterialReassignmentTab() + { + if (_editor.Files.Mdl.Count == 0) + return; + + using var tab = ImRaii.TabItem("Material Reassignment"); + if (!tab) + return; + + ImGui.NewLine(); + MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); + + ImGui.NewLine(); + using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + if (!child) + return; + + using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) + return; + + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) + { + using var id = ImRaii.PushId(idx); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) + info.Save(_editor.Compactor); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true)) + info.Restore(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + var tmp = info.CurrentMaterials[0]; + if (ImGui.InputText("##0", ref tmp, 64)) + info.SetMaterial(tmp, 0); + + for (var i = 1; i < info.Count; ++i) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + tmp = info.CurrentMaterials[i]; + if (ImGui.InputText($"##{i}", ref tmp, 64)) + info.SetMaterial(tmp, i); + } + } + } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 79a8ae34..e915a879 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -179,6 +179,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService DrawSwapTab(); _modMergeTab.Draw(); DrawDuplicatesTab(); + DrawMaterialReassignmentTab(); DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); From 5512e0cad2b273caca8f1b10b82a72be2274be80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 09:43:07 +0200 Subject: [PATCH 287/865] Revert no-lowercasing for the moment. --- Penumbra/Interop/PathResolving/PathResolver.cs | 1 + Penumbra/Mods/SubMods/SubMod.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 67ec4fc3..49035dc8 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,6 +52,7 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); + path = path.ToLower(); return category switch { // Only Interface collection. diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index f6b1be96..a8c37369 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -52,7 +52,7 @@ public static class SubMod if (files != null) foreach (var property in files.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p)) + if (Utf8GamePath.FromString(property.Name, out var p, true)) data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } @@ -60,7 +60,7 @@ public static class SubMod if (swaps != null) foreach (var property in swaps.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p)) + if (Utf8GamePath.FromString(property.Name, out var p, true)) data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } From ee801b637eb6a7b1c2092d0b5e54c532b20a462a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 29 Jul 2024 07:46:20 +0000 Subject: [PATCH 288/865] [CI] Updating repo.json for testing_1.2.0.17 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9c9e41db..adf152b0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.16", + "TestingAssemblyVersion": "1.2.0.17", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.16/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.17/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 7ceaeb826f88fc08f35f8e85cba7488a2644453a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 18:25:30 +0200 Subject: [PATCH 289/865] Move Material Reassignment to the back (and shoot it) --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index e915a879..b0e9af7f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -179,7 +179,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService DrawSwapTab(); _modMergeTab.Draw(); DrawDuplicatesTab(); - DrawMaterialReassignmentTab(); DrawQuickImportTab(); _modelTab.Draw(); _materialTab.Draw(); @@ -192,6 +191,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService } DrawMissingFilesTab(); + DrawMaterialReassignmentTab(); } /// A row of three buttonSizes and a help marker that can be used for material suffix changing. From 8518240bf919a7499953dad8b399114bfa10e1ea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 18:25:41 +0200 Subject: [PATCH 290/865] Improve knowledge window somewhat. --- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 37 +++++++++++-- Penumbra/UI/Knowledge/RaceCodeTab.cs | 70 +++++++++++++++++++----- 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index de1b36b8..b14949de 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -5,11 +5,12 @@ using Dalamud.Memory; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; +using Penumbra.String; namespace Penumbra.UI.Knowledge; /// Draw the progress information for import. -public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUiService +public sealed class KnowledgeWindow : Window, IUiService { private readonly IReadOnlyList _tabs = [ @@ -19,7 +20,16 @@ public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUi private IKnowledgeTab? _selected; private readonly byte[] _filterStore = new byte[256]; - private TerminatedByteString _filter = TerminatedByteString.Empty; + private ByteString _lower = ByteString.Empty; + + /// Draw the progress information for import. + public KnowledgeWindow() + : base("Penumbra Knowledge Window") + => SizeConstraints = new WindowSizeConstraints + { + MaximumSize = new Vector2(10000, 10000), + MinimumSize = new Vector2(400, 200), + }; public override void Draw() { @@ -30,15 +40,23 @@ public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUi private void DrawSelector() { - using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(250 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); + using var group = ImUtf8.Group(); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##Filter"u8, _filterStore, out TerminatedByteString filter, "Filter..."u8)) + _lower = ByteString.FromSpanUnsafe(filter, true, null, null).AsciiToLowerClone(); + } + + using var child = ImUtf8.Child("KnowledgeSelector"u8, new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetContentRegionAvail().Y), true); if (!child) return; - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - ImUtf8.InputText("##Filter"u8, _filterStore, out _filter, "Filter..."u8); - foreach (var tab in _tabs) { + if (!_lower.IsEmpty && tab.SearchTags.IndexOf(_lower.Span) < 0) + continue; + if (ImUtf8.Selectable(tab.Name, _selected == tab)) _selected = tab; } @@ -46,6 +64,13 @@ public sealed class KnowledgeWindow() : Window("Penumbra Knowledge Window"), IUi private void DrawMain() { + using var group = ImUtf8.Group(); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImUtf8.TextFramed(_selected == null ? "No Selection"u8 : _selected.Name, ImGui.GetColorU32(ImGuiCol.FrameBg), + new Vector2(ImGui.GetContentRegionAvail().X, 0)); + } + using var child = ImUtf8.Child("KnowledgeMain"u8, ImGui.GetContentRegionAvail(), true); if (!child || _selected == null) return; diff --git a/Penumbra/UI/Knowledge/RaceCodeTab.cs b/Penumbra/UI/Knowledge/RaceCodeTab.cs index 988506dd..36b048aa 100644 --- a/Penumbra/UI/Knowledge/RaceCodeTab.cs +++ b/Penumbra/UI/Knowledge/RaceCodeTab.cs @@ -4,7 +4,7 @@ using Penumbra.GameData.Enums; namespace Penumbra.UI.Knowledge; -public sealed class RaceCodeTab : IKnowledgeTab +public sealed class RaceCodeTab() : IKnowledgeTab { public ReadOnlySpan Name => "Race Codes"u8; @@ -14,29 +14,69 @@ public sealed class RaceCodeTab : IKnowledgeTab public void Draw() { - using var table = ImUtf8.Table("table"u8, 4, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; + var size = new Vector2((ImGui.GetContentRegionAvail().X - ImUtf8.ItemSpacing.X) / 2, 0); + using (var table = ImUtf8.Table("adults"u8, 4, ImGuiTableFlags.BordersOuter, size)) + { + if (!table) + return; - ImUtf8.TableHeader("Race Code"u8); - ImUtf8.TableHeader("Race"u8); - ImUtf8.TableHeader("Gender"u8); - ImUtf8.TableHeader("NPC"u8); + DrawHeaders(); + foreach (var gr in Enum.GetValues()) + { + var (gender, race) = gr.Split(); + if (gender is not Gender.Male and not Gender.Female || race is ModelRace.Unknown) + continue; - foreach (var genderRace in Enum.GetValues()) + DrawRow(gender, race, false); + } + } + + ImGui.SameLine(); + + using (var table = ImUtf8.Table("children"u8, 4, ImGuiTableFlags.BordersOuter, size)) + { + if (!table) + return; + + DrawHeaders(); + foreach (var race in (ReadOnlySpan) + [ModelRace.Midlander, ModelRace.Elezen, ModelRace.Miqote, ModelRace.AuRa, ModelRace.Unknown]) + { + foreach (var gender in (ReadOnlySpan) [Gender.Male, Gender.Female]) + DrawRow(gender, race, true); + } + } + + return; + + static void DrawHeaders() { ImGui.TableNextColumn(); - ImUtf8.Text(genderRace.ToRaceCode()); - - var (gender, race) = genderRace.Split(); + ImUtf8.TableHeader("Race"u8); ImGui.TableNextColumn(); - ImUtf8.Text($"{race}"); + ImUtf8.TableHeader("Gender"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Age"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Race Code"u8); + } + + static void DrawRow(Gender gender, ModelRace race, bool child) + { + var gr = child + ? Names.CombinedRace(gender is Gender.Male ? Gender.MaleNpc : Gender.FemaleNpc, race) + : Names.CombinedRace(gender, race); + ImGui.TableNextColumn(); + ImUtf8.Text(race.ToName()); ImGui.TableNextColumn(); - ImUtf8.Text($"{gender}"); + ImUtf8.Text(gender.ToName()); ImGui.TableNextColumn(); - ImUtf8.Text(((ushort)genderRace & 0xF) != 1 ? "NPC"u8 : "Normal"u8); + ImUtf8.Text(child ? "Child"u8 : "Adult"u8); + + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(gr.ToRaceCode()); } } } From 5270ad4d0d8637c9c6ff5d3ed715e486236c9a1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 29 Jul 2024 23:00:03 +0200 Subject: [PATCH 291/865] Update ImageSharp --- Penumbra/Penumbra.csproj | 2 +- Penumbra/packages.lock.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 70208737..24ffe469 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -79,7 +79,7 @@ - + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 42539e78..8e7106dd 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -44,9 +44,9 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.4, )", - "resolved": "3.1.4", - "contentHash": "lFIdxgGDA5iYkUMRFOze7BGLcdpoLFbR+a20kc1W7NepvzU7ejtxtWOg9RvgG7kb9tBoJ3ONYOK6kLil/dgF1w==" + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, "JetBrains.Annotations": { "type": "Transitive", From 70281c576e7b16ac5b7551e53472e625b7f44a91 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jul 2024 18:34:26 +0200 Subject: [PATCH 292/865] Update Submodules. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/OtterGui b/OtterGui index 87a53262..33ffd7cb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 87a532620d622a00e60059b5dd42a04f0319b5b5 +Subproject commit 33ffd7cba3e487e98e55adca1677354078089943 diff --git a/Penumbra.GameData b/Penumbra.GameData index f5a74c70..d8ebd63c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f5a74c70ad3861c5c66e1df6ae9a29fc7a0d736a +Subproject commit d8ebd63cec1ac12ea547fd37b6c32bdf9b3f57d1 diff --git a/Penumbra.String b/Penumbra.String index f04abbab..91f0f211 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit f04abbabedf5e757c5cbb970f3e513fef23e53cf +Subproject commit 91f0f21137c61bd39281debf88a8ecc494043330 From 9d128a4d831849c791dbce8efa9dbcda4c75f75f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jul 2024 18:38:25 +0200 Subject: [PATCH 293/865] Fix potential threading issue on launch. --- Penumbra/Interop/PathResolving/DrawObjectState.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 2a9ec7a9..5e413fe2 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; @@ -22,18 +23,19 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _gameState.LastGameObject; public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, - CharacterBaseDestructor characterBaseDestructor, GameState gameState) + CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework) { _objects = objects; _createCharacterBase = createCharacterBase; _weaponReload = weaponReload; _characterBaseDestructor = characterBaseDestructor; _gameState = gameState; + framework.RunOnFrameworkThread(InitializeDrawObjects); + _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); - InitializeDrawObjects(); } public bool ContainsKey(nint key) @@ -94,8 +96,8 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary private unsafe void InitializeDrawObjects() { - foreach(var actor in _objects) - { + foreach (var actor in _objects) + { if (actor is { IsCharacter: true, Model.Valid: true }) IterateDrawObjectTree((Object*)actor.Model.Address, actor, false, false); } From d247f83e1db8bf59ea647fdae3e9a22fb4996015 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 30 Jul 2024 18:53:55 +0200 Subject: [PATCH 294/865] Use CiByteString for anything path-related. --- Penumbra/Api/Api/ResolveApi.cs | 2 +- Penumbra/Api/Api/TemporaryApi.cs | 2 +- Penumbra/Api/DalamudSubstitutionProvider.cs | 3 +- Penumbra/Collections/Cache/ImcCache.cs | 6 ++-- Penumbra/Enums/ResourceTypeFlag.cs | 6 ++-- .../PostProcessing/PreBoneDeformerReplacer.cs | 3 +- .../Hooks/ResourceLoading/CreateFileWHook.cs | 2 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 21 +++++++------- .../Hooks/ResourceLoading/ResourceService.cs | 8 +++--- .../Interop/MaterialPreview/MaterialInfo.cs | 7 +++-- .../Interop/PathResolving/PathDataHandler.cs | 10 +++---- .../Interop/PathResolving/PathResolver.cs | 1 - Penumbra/Interop/PathResolving/PathState.cs | 2 +- .../Processing/AvfxPathPreProcessor.cs | 2 +- .../Processing/FilePostProcessService.cs | 4 +-- .../Processing/GamePathPreProcessService.cs | 4 +-- .../Processing/ImcFilePostProcessor.cs | 2 +- .../Interop/Processing/ImcPathPreProcessor.cs | 2 +- .../Processing/MaterialFilePostProcessor.cs | 2 +- .../Processing/MtrlPathPreProcessor.cs | 2 +- .../Interop/Processing/TmbPathPreProcessor.cs | 2 +- .../ResolveContext.PathResolution.cs | 9 +++--- .../Interop/ResourceTree/ResolveContext.cs | 20 ++++++------- Penumbra/Interop/ResourceTree/ResourceNode.cs | 12 ++++---- Penumbra/Interop/Services/DecalReverter.cs | 5 ++-- Penumbra/Interop/Structs/ResourceHandle.cs | 4 +-- Penumbra/Interop/Structs/StructExtensions.cs | 24 ++++++++-------- Penumbra/Mods/ModCreator.cs | 4 +-- Penumbra/Mods/SubMods/SubMod.cs | 4 +-- Penumbra/UI/AdvancedWindow/FileEditor.cs | 5 ++-- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 8 +++--- .../ModEditWindow.Materials.MtrlTab.cs | 4 +-- .../ModEditWindow.Models.MdlTab.cs | 2 +- .../ModEditWindow.ShaderPackages.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 28 ++++++++++++------- Penumbra/UI/Knowledge/KnowledgeWindow.cs | 2 -- Penumbra/UI/ResourceWatcher/Record.cs | 18 ++++++------ .../UI/ResourceWatcher/ResourceWatcher.cs | 4 +-- .../ResourceWatcher/ResourceWatcherTable.cs | 4 +-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 28 +++++++++++++++++++ Penumbra/UI/Tabs/EffectiveTab.cs | 5 ++-- 42 files changed, 163 insertions(+), 124 deletions(-) diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs index ec57eba7..481ea7ad 100644 --- a/Penumbra/Api/Api/ResolveApi.cs +++ b/Penumbra/Api/Api/ResolveApi.cs @@ -94,7 +94,7 @@ public class ResolveApi( if (!config.EnableMods) return path; - var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; + var gamePath = Utf8GamePath.FromString(path, out var p) ? p : Utf8GamePath.Empty; var ret = collection.ResolvePath(gamePath); return ret?.ToString() ?? path; } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 0894a8e5..f02b0d94 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -137,7 +137,7 @@ public class TemporaryApi( paths = new Dictionary(redirections.Count); foreach (var (gString, fString) in redirections) { - if (!Utf8GamePath.FromString(gString, out var path, false)) + if (!Utf8GamePath.FromString(gString, out var path)) { paths = null; return false; diff --git a/Penumbra/Api/DalamudSubstitutionProvider.cs b/Penumbra/Api/DalamudSubstitutionProvider.cs index 6347447a..e10dc461 100644 --- a/Penumbra/Api/DalamudSubstitutionProvider.cs +++ b/Penumbra/Api/DalamudSubstitutionProvider.cs @@ -4,7 +4,6 @@ using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Services; using Penumbra.String.Classes; @@ -130,7 +129,7 @@ public class DalamudSubstitutionProvider : IDisposable, IApiService try { - if (!Utf8GamePath.FromString(path, out var utf8Path, true)) + if (!Utf8GamePath.FromString(path, out var utf8Path)) return; var resolved = _activeCollectionData.Interface.ResolvePath(utf8Path); diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 40c3d2c7..cac52f99 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -8,12 +8,12 @@ namespace Penumbra.Collections.Cache; public sealed class ImcCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - private readonly Dictionary)> _imcFiles = []; + private readonly Dictionary)> _imcFiles = []; - public bool HasFile(ByteString path) + public bool HasFile(CiByteString path) => _imcFiles.ContainsKey(path); - public bool GetFile(ByteString path, [NotNullWhen(true)] out ImcFile? file) + public bool GetFile(CiByteString path, [NotNullWhen(true)] out ImcFile? file) { if (!_imcFiles.TryGetValue(path, out var p)) { diff --git a/Penumbra/Enums/ResourceTypeFlag.cs b/Penumbra/Enums/ResourceTypeFlag.cs index 461e7ac1..920e9780 100644 --- a/Penumbra/Enums/ResourceTypeFlag.cs +++ b/Penumbra/Enums/ResourceTypeFlag.cs @@ -216,10 +216,10 @@ public static class ResourceExtensions }; } - public static ResourceType Type(ByteString path) + public static ResourceType Type(CiByteString path) { var extIdx = path.LastIndexOf((byte)'.'); - var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? ByteString.Empty : path.Substring(extIdx + 1); + var ext = extIdx == -1 ? path : extIdx == path.Length - 1 ? CiByteString.Empty : path.Substring(extIdx + 1); return ext.Length switch { @@ -231,7 +231,7 @@ public static class ResourceExtensions }; } - public static ResourceCategory Category(ByteString path) + public static ResourceCategory Category(CiByteString path) { if (path.Length < 3) return ResourceCategory.Debug; diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 903484ea..834a7d28 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -7,6 +7,7 @@ using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; +using Penumbra.String; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; @@ -15,7 +16,7 @@ namespace Penumbra.Interop.Hooks.PostProcessing; public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredService { public static readonly Utf8GamePath PreBoneDeformerPath = - Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, out var p) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; // Approximate name guesses. private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); diff --git a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs index a8ac0608..8d0ac8cb 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs @@ -102,7 +102,7 @@ public unsafe class CreateFileWHook : IDisposable, IRequiredService { // Use static storage. var ptr = WriteFileName(name); - Penumbra.Log.Excessive($"[ResourceHooks] Calling CreateFileWDetour with {ByteString.FromSpanUnsafe(name, false)}."); + Penumbra.Log.Excessive($"[ResourceHooks] Calling CreateFileWDetour with {CiByteString.FromSpanUnsafe(name, false)}."); return _createFileWHook.OriginalDisposeSafe(ptr, access, shareMode, security, creation, flags, template); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 3f055f64..bcd09b37 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -41,7 +41,7 @@ public unsafe class ResourceLoader : IDisposable, IService private int PapResourceHandler(void* self, byte* path, int length) { - if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, out var gamePath)) + if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) return length; var (resolvedPath, data) = _incMode.Value @@ -64,7 +64,7 @@ public unsafe class ResourceLoader : IDisposable, IService } /// Load a resource for a given path and a specific collection. - public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { _resolvedData = resolveData; var ret = _resources.GetResource(category, type, path); @@ -73,7 +73,7 @@ public unsafe class ResourceLoader : IDisposable, IService } /// Load a resource for a given path and a specific collection. - public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, ByteString path, ResolveData resolveData) + public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { _resolvedData = resolveData; var ret = _resources.GetSafeResource(category, type, path); @@ -98,7 +98,7 @@ public unsafe class ResourceLoader : IDisposable, IService /// public event ResourceLoadedDelegate? ResourceLoaded; - public delegate void FileLoadedDelegate(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + public delegate void FileLoadedDelegate(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, ReadOnlySpan additionalData); /// @@ -172,7 +172,8 @@ public unsafe class ResourceLoader : IDisposable, IService return; } - var path = ByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, gamePath.Path.IsAscii); + var path = CiByteString.FromSpanUnsafe(actualPath, gamePath.Path.IsNullTerminated, gamePath.Path.IsAsciiLowerCase, + gamePath.Path.IsAscii); fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; MtrlForceSync(fileDescriptor, ref isSync); @@ -184,7 +185,7 @@ public unsafe class ResourceLoader : IDisposable, IService /// Load a resource by its path. If it is rooted, it will be loaded from the drive, otherwise from the SqPack. - private byte DefaultLoadResource(ByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, + private byte DefaultLoadResource(CiByteString gamePath, SeFileDescriptor* fileDescriptor, int priority, bool isSync, ReadOnlySpan additionalData) { if (Utf8GamePath.IsRooted(gamePath)) @@ -265,7 +266,7 @@ public unsafe class ResourceLoader : IDisposable, IService } /// Compute the CRC32 hash for a given path together with potential resource parameters. - private static int ComputeHash(ByteString path, GetResourceParameters* pGetResParams) + private static int ComputeHash(CiByteString path, GetResourceParameters* pGetResParams) { if (pGetResParams == null || !pGetResParams->IsPartialRead) return path.Crc32; @@ -273,11 +274,11 @@ public unsafe class ResourceLoader : IDisposable, IService // When the game requests file only partially, crc32 includes that information, in format of: // path/to/file.ext.hex_offset.hex_size // ex) music/ex4/BGM_EX4_System_Title.scd.381adc.30000 - return ByteString.Join( + return CiByteString.Join( (byte)'.', path, - ByteString.FromStringUnsafe(pGetResParams->SegmentOffset.ToString("x"), true), - ByteString.FromStringUnsafe(pGetResParams->SegmentLength.ToString("x"), true) + CiByteString.FromString(pGetResParams->SegmentOffset.ToString("x"), out var s1, MetaDataComputation.None) ? s1 : CiByteString.Empty, + CiByteString.FromString(pGetResParams->SegmentLength.ToString("x"), out var s2, MetaDataComputation.None) ? s2 : CiByteString.Empty ).Crc32; } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 0b00452b..8b99dc37 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -39,14 +39,14 @@ public unsafe class ResourceService : IDisposable, IRequiredService } } - public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, ByteString path) + public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, CiByteString path) { var hash = path.Crc32; return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress, &category, &type, &hash, path.Path, null, false); } - public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, ByteString path) + public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, CiByteString path) => new((CSResourceHandle*)GetResource(category, type, path), false); public void Dispose() @@ -102,7 +102,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) { using var performance = _performance.Measure(PerformanceType.GetResourceHandler); - if (!Utf8GamePath.FromPointer(path, out var gamePath)) + if (!Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) { Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path."); return isSync @@ -120,7 +120,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService } /// Call the original GetResource function. - public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, ByteString path, + public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, GetResourceParameters* resourceParameters = null, bool unk = false) => sync ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, diff --git a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs index f7e6caf0..f2ea2d6c 100644 --- a/Penumbra/Interop/MaterialPreview/MaterialInfo.cs +++ b/Penumbra/Interop/MaterialPreview/MaterialInfo.cs @@ -49,7 +49,10 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy public static unsafe List FindMaterials(IEnumerable gameObjects, string materialPath) { - var needle = ByteString.FromString(materialPath.Replace('\\', '/'), out var m, true) ? m : ByteString.Empty; + var needle = CiByteString.FromString(materialPath.Replace('\\', '/'), out var m, + MetaDataComputation.CiCrc32 | MetaDataComputation.Crc32) + ? m + : CiByteString.Empty; var result = new List(Enum.GetValues().Length); foreach (var objectPtr in gameObjects) @@ -83,7 +86,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy continue; PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _); - var fileName = ByteString.FromSpanUnsafe(path, true); + var fileName = CiByteString.FromSpanUnsafe(path, true); if (fileName == needle) result.Add(new MaterialInfo(index, type, i, j)); } diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index a8be97c8..9410ff98 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -31,27 +31,27 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateImc(ByteString path, ModCollection collection) + public static FullPath CreateImc(CiByteString path, ModCollection collection) => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateTmb(ByteString path, ModCollection collection) + public static FullPath CreateTmb(CiByteString path, ModCollection collection) => CreateBase(path, collection); /// Create the encoding path for an AVFX file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateAvfx(ByteString path, ModCollection collection) + public static FullPath CreateAvfx(CiByteString path, ModCollection collection) => CreateBase(path, collection); /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static FullPath CreateMtrl(ByteString path, ModCollection collection, Utf8GamePath originalPath) + public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); /// The base function shared by most file types. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static FullPath CreateBase(ByteString path, ModCollection collection) + private static FullPath CreateBase(CiByteString path, ModCollection collection) => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}"); /// Read an additional data blurb and parse it into usable data for all file types but Materials. diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 49035dc8..67ec4fc3 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,7 +52,6 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); - path = path.ToLower(); return category switch { // Only Interface collection. diff --git a/Penumbra/Interop/PathResolving/PathState.cs b/Penumbra/Interop/PathResolving/PathState.cs index bf9d1e25..60a61408 100644 --- a/Penumbra/Interop/PathResolving/PathState.cs +++ b/Penumbra/Interop/PathResolving/PathState.cs @@ -28,7 +28,7 @@ public sealed class PathState(CollectionResolver collectionResolver, MetaState m _internalResolve.Dispose(); } - public bool Consume(ByteString _, out ResolveData collection) + public bool Consume(CiByteString _, out ResolveData collection) { if (_resolveData.IsValueCreated) { diff --git a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs index 56f693e6..2194354a 100644 --- a/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/AvfxPathPreProcessor.cs @@ -11,6 +11,6 @@ public sealed class AvfxPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Avfx; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) => nonDefault ? PathDataHandler.CreateAvfx(path, resolveData.ModCollection) : resolved; } diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index bba53c94..ecf78c69 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -10,7 +10,7 @@ namespace Penumbra.Interop.Processing; public interface IFilePostProcessor : IService { public ResourceType Type { get; } - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData); + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData); } public unsafe class FilePostProcessService : IRequiredService, IDisposable @@ -30,7 +30,7 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable _resourceLoader.FileLoaded -= OnFileLoaded; } - private void OnFileLoaded(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, + private void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, ReadOnlySpan additionalData) { if (_processors.TryGetValue(resource->FileType, out var processor)) diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs index 004b7168..65608ba0 100644 --- a/Penumbra/Interop/Processing/GamePathPreProcessService.cs +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -11,7 +11,7 @@ public interface IPathPreProcessor : IService { public ResourceType Type { get; } - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved); + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved); } public class GamePathPreProcessService : IService @@ -24,7 +24,7 @@ public class GamePathPreProcessService : IService } - public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, + public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type, FullPath? resolved, Utf8GamePath originalPath) { diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index 4a0ebe22..33a3941a 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -11,7 +11,7 @@ public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFileP public ResourceType Type => ResourceType.Imc; - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) { if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) return; diff --git a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs index 907d7587..7030dd8d 100644 --- a/Penumbra/Interop/Processing/ImcPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ImcPathPreProcessor.cs @@ -11,7 +11,7 @@ public sealed class ImcPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Imc; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool _, FullPath? resolved) => resolveData.ModCollection.MetaCache?.Imc.HasFile(originalGamePath.Path) ?? false ? PathDataHandler.CreateImc(path, resolveData.ModCollection) : resolved; diff --git a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs index 02b5d46c..26956845 100644 --- a/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/MaterialFilePostProcessor.cs @@ -10,7 +10,7 @@ public sealed class MaterialFilePostProcessor //: IFilePostProcessor public ResourceType Type => ResourceType.Mtrl; - public unsafe void PostProcess(ResourceHandle* resource, ByteString originalGamePath, ReadOnlySpan additionalData) + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) { if (!PathDataHandler.ReadMtrl(additionalData, out var data)) return; diff --git a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs index 8fb2400b..603781ed 100644 --- a/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/MtrlPathPreProcessor.cs @@ -11,6 +11,6 @@ public sealed class MtrlPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Mtrl; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) => nonDefault ? PathDataHandler.CreateMtrl(path, resolveData.ModCollection, originalGamePath) : resolved; } diff --git a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs index dd887819..0a7aa16f 100644 --- a/Penumbra/Interop/Processing/TmbPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/TmbPathPreProcessor.cs @@ -11,6 +11,6 @@ public sealed class TmbPathPreProcessor : IPathPreProcessor public ResourceType Type => ResourceType.Tmb; - public FullPath? PreProcess(ResolveData resolveData, ByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) => nonDefault ? PathDataHandler.CreateTmb(path, resolveData.ModCollection) : resolved; } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 678dd8a9..85b3284a 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -3,7 +3,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String; @@ -99,7 +98,7 @@ internal partial record ResolveContext Span pathBuffer = stackalloc byte[260]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } [SkipLocalsInit] @@ -133,7 +132,7 @@ internal partial record ResolveContext if (weaponPosition >= 0) WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); - return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } } @@ -148,7 +147,7 @@ internal partial record ResolveContext Span pathBuffer = stackalloc byte[260]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty; + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant) @@ -196,7 +195,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMaterialPathNative(byte* mtrlFileName) { - ByteString? path; + CiByteString? path; try { path = CharacterBase->ResolveMtrlPathAsByteString(SlotIndex, mtrlFileName); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index acb320d4..3fc1ae3c 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -45,25 +45,25 @@ internal unsafe partial record ResolveContext( public CharacterBase* CharacterBase => CharacterBasePointer.Value; - private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); private ModelType ModelType => CharacterBase->GetModelType(); - private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, ByteString gamePath) + private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) { if (resourceHandle == null) return null; if (gamePath.IsEmpty) return null; - if (!Utf8GamePath.FromByteString(ByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path, false)) + if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path)) return null; return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path); } [SkipLocalsInit] - private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11) + private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11) { if (resourceHandle == null) return null; @@ -81,7 +81,7 @@ internal unsafe partial record ResolveContext( prefixed[lastDirectorySeparator + 2] = (byte)'-'; gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); - if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], out var tmp)) + if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) return null; path = tmp.Clone(); @@ -118,11 +118,11 @@ internal unsafe partial record ResolveContext( throw new ArgumentNullException(nameof(resourceHandle)); var fileName = (ReadOnlySpan)resourceHandle->FileName.AsSpan(); - var additionalData = ByteString.Empty; + var additionalData = CiByteString.Empty; if (PathDataHandler.Split(fileName, out fileName, out var data)) - additionalData = ByteString.FromSpanUnsafe(data, false).Clone(); + additionalData = CiByteString.FromSpanUnsafe(data, false).Clone(); - var fullPath = Utf8GamePath.FromSpan(fileName, out var p) ? new FullPath(p.Clone()) : FullPath.Empty; + var fullPath = Utf8GamePath.FromSpan(fileName, MetaDataComputation.None, out var p) ? new FullPath(p.Clone()) : FullPath.Empty; var node = new ResourceNode(type, objectAddress, (nint)resourceHandle, GetResourceHandleLength(resourceHandle), this) { @@ -222,7 +222,7 @@ internal unsafe partial record ResolveContext( return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new ByteString(resource->ShpkName)); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName)); if (shpkNode != null) { if (Global.WithUiData) @@ -236,7 +236,7 @@ internal unsafe partial record ResolveContext( var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new ByteString(resource->TexturePath(i)), + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new CiByteString(resource->TexturePath(i)), resource->Textures[i].IsDX11); if (texNode == null) continue; diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 9c911791..de43a874 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -15,7 +15,7 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; - public ByteString AdditionalData; + public CiByteString AdditionalData; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; @@ -26,9 +26,9 @@ public class ResourceNode : ICloneable set { if (value.IsEmpty) - PossibleGamePaths = Array.Empty(); + PossibleGamePaths = []; else - PossibleGamePaths = new[] { value }; + PossibleGamePaths = [value]; } } @@ -40,8 +40,8 @@ public class ResourceNode : ICloneable Type = type; ObjectAddress = objectAddress; ResourceHandle = resourceHandle; - PossibleGamePaths = Array.Empty(); - AdditionalData = ByteString.Empty; + PossibleGamePaths = []; + AdditionalData = CiByteString.Empty; Length = length; Children = new List(); ResolveContext = resolveContext; @@ -90,7 +90,7 @@ public class ResourceNode : ICloneable public readonly record struct UiData(string? Name, ChangedItemIcon Icon) { - public readonly UiData PrependName(string prefix) + public UiData PrependName(string prefix) => Name == null ? this : new UiData(prefix + Name, Icon); } } diff --git a/Penumbra/Interop/Services/DecalReverter.cs b/Penumbra/Interop/Services/DecalReverter.cs index 21b51fd2..3d5d7845 100644 --- a/Penumbra/Interop/Services/DecalReverter.cs +++ b/Penumbra/Interop/Services/DecalReverter.cs @@ -2,6 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.String; using Penumbra.String.Classes; namespace Penumbra.Interop.Services; @@ -9,10 +10,10 @@ namespace Penumbra.Interop.Services; public sealed unsafe class DecalReverter : IDisposable { public static readonly Utf8GamePath DecalPath = - Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, out var p) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/common/texture/decal_equip/_stigma.tex"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; public static readonly Utf8GamePath TransparentPath = - Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, out var p) ? p : Utf8GamePath.Empty; + Utf8GamePath.FromSpan("chara/common/texture/transparent.tex"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; private readonly CharacterUtility _utility; private readonly Structs.TextureResourceHandle* _decal; diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 6e428f25..65550563 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -47,11 +47,11 @@ public unsafe struct ResourceHandle public ulong DataLength; } - public readonly ByteString FileName() + public readonly CiByteString FileName() => CsHandle.FileName.AsByteString(); public readonly bool GamePath(out Utf8GamePath path) - => Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), out path); + => Utf8GamePath.FromSpan(CsHandle.FileName.AsSpan(), MetaDataComputation.All, out path); [FieldOffset(0x00)] public CsHandle.ResourceHandle CsHandle; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index fc8b1c3d..9dd9a96d 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -6,48 +6,48 @@ namespace Penumbra.Interop.Structs; internal static class StructExtensions { - public static unsafe ByteString AsByteString(in this StdString str) - => ByteString.FromSpanUnsafe(str.AsSpan(), true); + public static CiByteString AsByteString(in this StdString str) + => CiByteString.FromSpanUnsafe(str.AsSpan(), true); - public static ByteString ResolveEidPathAsByteString(ref this CharacterBase character) + public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveEidPath(pathBuffer)); } - public static ByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint 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 ByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex) + 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 ByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName) + 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 ByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint 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 ByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); } - private static unsafe ByteString ToOwnedByteString(byte* str) - => str == null ? ByteString.Empty : new ByteString(str).Clone(); + private static unsafe CiByteString ToOwnedByteString(byte* str) + => str == null ? CiByteString.Empty : new CiByteString(str).Clone(); - private static ByteString ToOwnedByteString(ReadOnlySpan str) - => str.Length == 0 ? ByteString.Empty : ByteString.FromSpanUnsafe(str, true).Clone(); + private static CiByteString ToOwnedByteString(ReadOnlySpan str) + => str.Length == 0 ? CiByteString.Empty : CiByteString.FromSpanUnsafe(str, true).Clone(); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 546f5f5c..0f4972e3 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -270,7 +270,7 @@ public partial class ModCreator( public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority) { var list = optionFolder.EnumerateNonHiddenFiles() - .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) + .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath), gamePath, new FullPath(f))) .Where(t => t.Item1); var mod = MultiSubMod.WithoutGroup(option.Name, option.Description, priority); @@ -291,7 +291,7 @@ public partial class ModCreator( ReloadMod(mod, false, out _); foreach (var file in mod.FindUnusedFiles()) { - if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) + if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath)) mod.Default.Files.TryAdd(gamePath, file); } diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a8c37369..f6b1be96 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -52,7 +52,7 @@ public static class SubMod if (files != null) foreach (var property in files.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); } @@ -60,7 +60,7 @@ public static class SubMod if (swaps != null) foreach (var property in swaps.Properties()) { - if (Utf8GamePath.FromString(property.Name, out var p, true)) + if (Utf8GamePath.FromString(property.Name, out var p)) data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 2c6ac170..c783e17f 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -6,6 +6,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Compression; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Files; using Penumbra.Mods.Editor; @@ -98,7 +99,7 @@ public class FileEditor( _inInput = ImGui.IsItemActive(); if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) { - _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true); + _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8); _quickImport = null; fileDialog.Reset(); try @@ -306,7 +307,7 @@ public class FileEditor( foreach (var (option, gamePath) in file.SubModUsage) { ImGui.TableNextColumn(); - UiHelpers.Text(gamePath.Path); + ImUtf8.Text(gamePath.Path.Span); ImGui.TableNextColumn(); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); ImGui.TextUnformatted(option.GetFullName()); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 107c56e6..ffa7473d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -209,7 +209,7 @@ public partial class ModEditWindow if (ImGui.IsItemDeactivatedAfterEdit()) { - if (Utf8GamePath.FromString(_gamePathEdit, out var path, false)) + if (Utf8GamePath.FromString(_gamePathEdit, out var path)) _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path); _fileIdx = -1; @@ -217,7 +217,7 @@ public partial class ModEditWindow } else if (_fileIdx == i && _pathIdx == j - && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + && (!Utf8GamePath.FromString(_gamePathEdit, out var path) || !path.IsEmpty && !path.Equals(gamePath) && !_editor.FileEditor.CanAddGamePath(path))) { ImGui.SameLine(); @@ -241,7 +241,7 @@ public partial class ModEditWindow if (ImGui.IsItemDeactivatedAfterEdit()) { - if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty) + if (Utf8GamePath.FromString(_gamePathEdit, out var path) && !path.IsEmpty) _editor.FileEditor.SetGamePath(_editor.Option!, _fileIdx, _pathIdx, path); _fileIdx = -1; @@ -249,7 +249,7 @@ public partial class ModEditWindow } else if (_fileIdx == i && _pathIdx == -1 - && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + && (!Utf8GamePath.FromString(_gamePathEdit, out var path) || !path.IsEmpty && !_editor.FileEditor.CanAddGamePath(path))) { ImGui.SameLine(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index cd7aca9d..a50599a1 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -25,7 +25,7 @@ public partial class ModEditWindow { private const int ShpkPrefixLength = 16; - private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); private readonly ModEditWindow _edit; public readonly MtrlFile Mtrl; @@ -77,7 +77,7 @@ public partial class ModEditWindow public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath, true)) + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) return FullPath.Empty; return _edit.FindBestMatch(defaultGamePath); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b05bcac2..b436448f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -271,7 +271,7 @@ public partial class ModEditWindow private byte[]? ReadFile(string path) { // TODO: if cross-collection lookups are turned off, this conversion can be skipped - if (!Utf8GamePath.FromString(path, out var utf8Path, true)) + if (!Utf8GamePath.FromString(path, out var utf8Path)) throw new Exception($"Resolved path {path} could not be converted to a game path."); var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8Path) ?? new FullPath(utf8Path); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index a22c10ad..017478a7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); + private static readonly CiByteString DisassemblyLabel = CiByteString.FromSpanUnsafe("##disassembly"u8, true, true, true); private readonly FileEditor _shaderPackageTab; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index b0e9af7f..0d3dce8c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -562,7 +562,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return new FullPath(path); } - private HashSet FindPathsStartingWith(ByteString prefix) + private HashSet FindPathsStartingWith(CiByteString prefix) { var ret = new HashSet(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 7315f136..c47414b9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -84,7 +84,8 @@ public class ResourceTreeViewer using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { - var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); + var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", + index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) { using var _ = ImRaii.PushFont(UiBuilder.MonoFont); @@ -149,7 +150,9 @@ public class ResourceTreeViewer var filterChanged = false; ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) + { filterChanged |= _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + } var fieldWidth = (ImGui.GetContentRegionAvail().X - checkSpacing * 2.0f - ImGui.GetFrameHeightWithSpacing()) / 2.0f; ImGui.SetNextItemWidth(fieldWidth); @@ -181,7 +184,8 @@ public class ResourceTreeViewer } }); - private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, + ChangedItemDrawer.ChangedItemIcon parentFilterIcon) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); @@ -196,9 +200,9 @@ public class ResourceTreeViewer return true; return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); } NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) @@ -226,10 +230,11 @@ public class ResourceTreeViewer visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); _filterCache.Add(nodePathHash, visibility); } + return visibility; } - string GetAdditionalDataSuffix(ByteString data) + string GetAdditionalDataSuffix(CiByteString data) => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) @@ -252,8 +257,9 @@ public class ResourceTreeViewer var unfolded = _unfolded.Contains(nodePathHash); using (var indent = ImRaii.PushIndent(level)) { - var hasVisibleChildren = resourceNode.Children.Any(child => GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden); - var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; + var hasVisibleChildren = resourceNode.Children.Any(child + => GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden); + var unfoldable = hasVisibleChildren && visibility != NodeVisibility.DescendentsOnly; if (unfoldable) { using var font = ImRaii.PushFont(UiBuilder.IconFont); @@ -317,13 +323,15 @@ public class ResourceTreeViewer ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); - ImGuiUtil.HoverTooltip($"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + ImGuiUtil.HoverTooltip( + $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } else { ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); - ImGuiUtil.HoverTooltip($"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + ImGuiUtil.HoverTooltip( + $"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); diff --git a/Penumbra/UI/Knowledge/KnowledgeWindow.cs b/Penumbra/UI/Knowledge/KnowledgeWindow.cs index b14949de..f831975b 100644 --- a/Penumbra/UI/Knowledge/KnowledgeWindow.cs +++ b/Penumbra/UI/Knowledge/KnowledgeWindow.cs @@ -1,7 +1,5 @@ -using System.Text.Unicode; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Memory; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 0fc51f26..b69d9944 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -18,8 +18,8 @@ public enum RecordType : byte internal unsafe struct Record { public DateTime Time; - public ByteString Path; - public ByteString OriginalPath; + public CiByteString Path; + public CiByteString OriginalPath; public string AssociatedGameObject; public ModCollection? Collection; public ResourceHandle* Handle; @@ -32,12 +32,12 @@ internal unsafe struct Record public OptionalBool CustomLoad; public LoadState LoadState; - public static Record CreateRequest(ByteString path, bool sync) + public static Record CreateRequest(CiByteString path, bool sync) => new() { Time = DateTime.UtcNow, Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, + OriginalPath = CiByteString.Empty, Collection = null, Handle = null, ResourceType = ResourceExtensions.Type(path).ToFlag(), @@ -51,7 +51,7 @@ internal unsafe struct Record LoadState = LoadState.None, }; - public static Record CreateDefaultLoad(ByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) + public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { path = path.IsOwned ? path : path.Clone(); return new Record @@ -73,7 +73,7 @@ internal unsafe struct Record }; } - public static Record CreateLoad(ByteString path, ByteString originalPath, ResourceHandle* handle, ModCollection collection, + public static Record CreateLoad(CiByteString path, CiByteString originalPath, ResourceHandle* handle, ModCollection collection, string associatedGameObject) => new() { @@ -100,7 +100,7 @@ internal unsafe struct Record { Time = DateTime.UtcNow, Path = path, - OriginalPath = ByteString.Empty, + OriginalPath = CiByteString.Empty, Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -115,12 +115,12 @@ internal unsafe struct Record }; } - public static Record CreateFileLoad(ByteString path, ResourceHandle* handle, bool ret, bool custom) + public static Record CreateFileLoad(CiByteString path, ResourceHandle* handle, bool ret, bool custom) => new() { Time = DateTime.UtcNow, Path = path.IsOwned ? path : path.Clone(), - OriginalPath = ByteString.Empty, + OriginalPath = CiByteString.Empty, Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 14d69489..6f1ce9cf 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -163,7 +163,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService } } - private bool FilterMatch(ByteString path, out string match) + private bool FilterMatch(CiByteString path, out string match) { match = path.ToString(); return _logFilter.Length == 0 || (_logRegex?.IsMatch(match) ?? false) || match.Contains(_logFilter, StringComparison.OrdinalIgnoreCase); @@ -255,7 +255,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } - private unsafe void OnFileLoaded(ResourceHandle* resource, ByteString path, bool success, bool custom, ReadOnlySpan _) + private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) Penumbra.Log.Information( diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index b47574d0..33e301ae 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -50,7 +50,7 @@ internal sealed class ResourceWatcherTable : Table => DrawByteString(item.Path, 280 * UiHelpers.Scale); } - private static unsafe void DrawByteString(ByteString path, float length) + private static unsafe void DrawByteString(CiByteString path, float length) { Vector2 vec; ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); @@ -61,7 +61,7 @@ internal sealed class ResourceWatcherTable : Table else { var fileName = path.LastIndexOf((byte)'/'); - ByteString shortPath; + CiByteString shortPath; if (fileName != -1) { using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 4966dd64..a1e9da03 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections.Manager; @@ -402,6 +403,33 @@ public class DebugTab : Window, ITab, IUiService } } } + + using (var tree = ImUtf8.TreeNode("String Memory"u8)) + { + if (tree) + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Currently Allocated Strings"u8); + ImUtf8.Text("Total Allocated Strings"u8); + ImUtf8.Text("Free'd Allocated Strings"u8); + ImUtf8.Text("Currently Allocated Bytes"u8); + ImUtf8.Text("Total Allocated Bytes"u8); + ImUtf8.Text("Free'd Allocated Bytes"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{PenumbraStringMemory.CurrentStrings}"); + ImUtf8.Text($"{PenumbraStringMemory.AllocatedStrings}"); + ImUtf8.Text($"{PenumbraStringMemory.FreedStrings}"); + ImUtf8.Text($"{PenumbraStringMemory.CurrentBytes}"); + ImUtf8.Text($"{PenumbraStringMemory.AllocatedBytes}"); + ImUtf8.Text($"{PenumbraStringMemory.FreedBytes}"); + } + } + } } private void DrawPerformanceTab() diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index e0cab43f..ecf9a886 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -134,12 +135,12 @@ public class EffectiveTab(CollectionManager collectionManager, CollectionSelectH { var (path, name) = pair; ImGui.TableNextColumn(); - UiHelpers.CopyOnClickSelectable(path.Path); + ImUtf8.CopyOnClickSelectable(path.Path.Span); ImGui.TableNextColumn(); ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); ImGui.TableNextColumn(); - UiHelpers.CopyOnClickSelectable(name.Path.InternalName); + ImUtf8.CopyOnClickSelectable(name.Path.InternalName.Span); ImGuiUtil.HoverTooltip($"\nChanged by {name.Mod.Name}."); } From 4b9870f09089d58e9171fa39511f93e7c8c9cbc0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 Jul 2024 23:09:37 +0200 Subject: [PATCH 295/865] Fix some OtterGui changes. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../ModEditWindow.Materials.MtrlTab.cs | 2 +- .../UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ImcManipulationDrawer.cs | 14 -------------- Penumbra/UI/Tabs/ResourceTab.cs | 4 ++-- 6 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 Penumbra/UI/ModsTab/ImcManipulationDrawer.cs diff --git a/OtterGui b/OtterGui index 33ffd7cb..b0464b7f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 33ffd7cba3e487e98e55adca1677354078089943 +Subproject commit b0464b7f215a0db1393e600968c6666307a3ae05 diff --git a/Penumbra.GameData b/Penumbra.GameData index d8ebd63c..75582ece 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d8ebd63cec1ac12ea547fd37b6c32bdf9b3f57d1 +Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index a50599a1..29fd7531 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -405,7 +405,7 @@ public partial class ModEditWindow } var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(true)) + foreach (var (start, end) in handledElements.Ranges(complement:true)) { if ((shpkConstant.ByteOffset & 0x3) == 0) { diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 5d10febd..bbb5e54e 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -140,7 +140,7 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr var value = (mask & (1 << i)) != 0; using (ImRaii.Disabled(!cache.CanChange(i))) { - if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + if (ImUtf8.Checkbox(""u8, ref value)) { if (data is ImcModGroup g) editor.ChangeDefaultAttribute(g, cache, i, value); diff --git a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs b/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs deleted file mode 100644 index 1291f568..00000000 --- a/Penumbra/UI/ModsTab/ImcManipulationDrawer.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ImGuiNET; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; -using Penumbra.Meta.Manipulations; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public static class ImcManipulationDrawer -{ - -} diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index a4dbba2f..c54e3433 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -81,7 +81,7 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa return; var address = $"0x{(ulong)r:X}"; - ImGuiUtil.TextNextColumn($"0x{hash:X8}"); + ImGuiUtil.DrawTableColumn($"0x{hash:X8}"); ImGui.TableNextColumn(); ImGuiUtil.CopyOnClickSelectable(address); @@ -101,7 +101,7 @@ public class ResourceTab(Configuration config, ResourceManagerService resourceMa ImGuiUtil.HoverTooltip("Click to copy byte-wise file data to clipboard, if any."); - ImGuiUtil.TextNextColumn(r->RefCount.ToString()); + ImGuiUtil.DrawTableColumn(r->RefCount.ToString()); }); } From 67a220f821afac69aeff24bc8e49ec7cc3dcb1b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 31 Jul 2024 23:24:59 +0200 Subject: [PATCH 296/865] Add context menu to change mod state from Collections tab. --- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 86 ++++++++++++++----- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 9f37f847..b7648428 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -11,9 +12,16 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSelector selector) : ITab, IUiService +public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab, IUiService { - private readonly List<(ModCollection, ModCollection, uint, string)> _cache = []; + private enum ModState + { + Enabled, + Disabled, + Unconfigured, + } + + private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = []; public ReadOnlySpan Label => "Collections"u8; @@ -23,45 +31,83 @@ public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSele var (direct, inherited) = CountUsage(selector.Selected!); ImGui.NewLine(); if (direct == 1) - ImGui.TextUnformatted("This Mod is directly configured in 1 collection."); + ImUtf8.Text("This Mod is directly configured in 1 collection."u8); else if (direct == 0) - ImGuiUtil.TextColored(Colors.RegexWarningBorder, "This mod is entirely unused."); + ImUtf8.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); else - ImGui.TextUnformatted($"This Mod is directly configured in {direct} collections."); + ImUtf8.Text($"This Mod is directly configured in {direct} collections."); if (inherited > 0) - ImGui.TextUnformatted( - $"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); + ImUtf8.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); ImGui.NewLine(); ImGui.Separator(); ImGui.NewLine(); - using var table = ImRaii.Table("##modCollections", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##modCollections"u8, 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) return; - var size = ImGui.CalcTextSize("Unconfigured").X + 20 * ImGuiHelpers.GlobalScale; + var size = ImUtf8.CalcTextSize(ToText(ModState.Unconfigured)).X + 20 * ImGuiHelpers.GlobalScale; var collectionSize = 200 * ImGuiHelpers.GlobalScale; ImGui.TableSetupColumn("Collection", ImGuiTableColumnFlags.WidthFixed, collectionSize); ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, size); ImGui.TableSetupColumn("Inherited From", ImGuiTableColumnFlags.WidthFixed, collectionSize); ImGui.TableHeadersRow(); - foreach (var (collection, parent, color, text) in _cache) + foreach (var ((collection, parent, color, state), idx) in _cache.WithIndex()) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(collection.Name); + using var id = ImUtf8.PushId(idx); + ImUtf8.DrawTableColumn(collection.Name); ImGui.TableNextColumn(); - using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) + ImUtf8.Text(ToText(state), color); + + using (var context = ImUtf8.PopupContextItem("Context"u8, ImGuiPopupFlags.MouseButtonRight)) { - ImGui.TextUnformatted(text); + if (context) + { + ImUtf8.Text(collection.Name); + ImGui.Separator(); + using (ImRaii.Disabled(state is ModState.Enabled && parent == collection)) + { + if (ImUtf8.MenuItem("Enable"u8)) + { + if (parent != collection) + manager.Editor.SetModInheritance(collection, selector.Selected!, false); + manager.Editor.SetModState(collection, selector.Selected!, true); + } + } + + using (ImRaii.Disabled(state is ModState.Disabled && parent == collection)) + { + if (ImUtf8.MenuItem("Disable"u8)) + { + if (parent != collection) + manager.Editor.SetModInheritance(collection, selector.Selected!, false); + manager.Editor.SetModState(collection, selector.Selected!, false); + } + } + + using (ImRaii.Disabled(parent != collection)) + { + if (ImUtf8.MenuItem("Inherit"u8)) + manager.Editor.SetModInheritance(collection, selector.Selected!, true); + } + } } - ImGui.TableNextColumn(); - ImGui.TextUnformatted(parent == collection ? string.Empty : parent.Name); + ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Name); } } + private static ReadOnlySpan ToText(ModState state) + => state switch + { + ModState.Unconfigured => "Unconfigured"u8, + ModState.Enabled => "Enabled"u8, + ModState.Disabled => "Disabled"u8, + _ => "Unknown"u8, + }; + private (int Direct, int Inherited) CountUsage(Mod mod) { _cache.Clear(); @@ -72,14 +118,14 @@ public class ModPanelCollectionsTab(CollectionStorage storage, ModFileSystemSele var disInherited = ColorId.InheritedDisabledMod.Value(); var directCount = 0; var inheritedCount = 0; - foreach (var collection in storage) + foreach (var collection in manager.Storage) { var (settings, parent) = collection[mod.Index]; var (color, text) = settings == null - ? (undefined, "Unconfigured") + ? (undefined, ModState.Unconfigured) : settings.Enabled - ? (parent == collection ? enabled : inherited, "Enabled") - : (parent == collection ? disabled : disInherited, "Disabled"); + ? (parent == collection ? enabled : inherited, ModState.Enabled) + : (parent == collection ? disabled : disInherited, ModState.Disabled); _cache.Add((collection, parent, color, text)); if (color == enabled) From 9e15865a99fd78240943f6d11bd784c2c122ca1a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 16:37:31 +0200 Subject: [PATCH 297/865] Fix some further issues with empty byte strings. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index b0464b7f..2b79faac 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit b0464b7f215a0db1393e600968c6666307a3ae05 +Subproject commit 2b79faacff30a31e9ad4b0a3c5d57ffd6e34cfa4 From a308fb9f779acf939191cf3c10c8468a440d7ba9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 16:40:37 +0200 Subject: [PATCH 298/865] Allow hook overrides. --- .../Animation/ApricotListenerSoundPlay.cs | 2 +- .../Animation/CharacterBaseLoadAnimation.cs | 3 +- Penumbra/Interop/Hooks/Animation/Dismount.cs | 2 +- .../Interop/Hooks/Animation/LoadAreaVfx.cs | 6 +- .../Hooks/Animation/LoadCharacterSound.cs | 2 +- .../Hooks/Animation/LoadCharacterVfx.cs | 2 +- .../Hooks/Animation/LoadTimelineResources.cs | 2 +- .../Interop/Hooks/Animation/PlayFootstep.cs | 2 +- .../Hooks/Animation/ScheduleClipUpdate.cs | 2 +- .../Interop/Hooks/Animation/SomeActionLoad.cs | 2 +- .../Hooks/Animation/SomeMountAnimation.cs | 2 +- .../Interop/Hooks/Animation/SomePapLoad.cs | 2 +- .../Hooks/Animation/SomeParasolAnimation.cs | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 144 ++++++++++++++++-- .../Interop/Hooks/Meta/CalculateHeight.cs | 2 +- .../Interop/Hooks/Meta/ChangeCustomize.cs | 2 +- .../Interop/Hooks/Meta/EqdpAccessoryHook.cs | 6 +- Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs | 5 +- Penumbra/Interop/Hooks/Meta/EqpHook.cs | 6 +- Penumbra/Interop/Hooks/Meta/EstHook.cs | 5 +- Penumbra/Interop/Hooks/Meta/GmpHook.cs | 6 +- .../Interop/Hooks/Meta/ModelLoadComplete.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspBustHook.cs | 10 +- Penumbra/Interop/Hooks/Meta/RspHeightHook.cs | 9 +- .../Interop/Hooks/Meta/RspSetupCharacter.cs | 2 +- Penumbra/Interop/Hooks/Meta/RspTailHook.cs | 10 +- Penumbra/Interop/Hooks/Meta/SetupVisor.cs | 2 +- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- Penumbra/Interop/Hooks/Meta/UpdateRender.cs | 4 +- .../Hooks/Objects/CharacterBaseDestructor.cs | 2 +- .../Hooks/Objects/CharacterDestructor.cs | 2 +- .../Interop/Hooks/Objects/CopyCharacter.cs | 2 +- .../Hooks/Objects/CreateCharacterBase.cs | 2 +- Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/WeaponReload.cs | 2 +- .../PostProcessing/PreBoneDeformerReplacer.cs | 4 +- .../PostProcessing/ShaderReplacementFixer.cs | 34 +++-- .../Hooks/ResourceLoading/CreateFileWHook.cs | 2 +- .../Hooks/ResourceLoading/FileReadService.cs | 2 +- .../Hooks/ResourceLoading/MappedCodeReader.cs | 11 +- .../Hooks/ResourceLoading/PapHandler.cs | 3 + .../Hooks/ResourceLoading/PeSigScanner.cs | 1 - .../Hooks/ResourceLoading/ResourceService.cs | 7 +- .../Hooks/ResourceLoading/TexMdlService.cs | 6 +- .../Hooks/Resources/ApricotResourceLoad.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlShpk.cs | 2 +- .../Interop/Hooks/Resources/LoadMtrlTex.cs | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 2 +- .../Resources/ResourceHandleDestructor.cs | 3 +- Penumbra/Penumbra.cs | 9 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 21 +-- Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 63 ++++++++ 52 files changed, 326 insertions(+), 108 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 96a51027..44eb7ebb 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -24,7 +24,7 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook("Apricot Listener Sound Play Caller", Sigs.ApricotListenerSoundPlayCaller, Detour, - true); //HookSettings.VfxIdentificationHooks); + !HookOverrides.Instance.Animation.ApricotListenerSoundPlayCaller); } public delegate nint Delegate(nint a1, nint a2, float a3); diff --git a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs index f99d8ca4..22609afc 100644 --- a/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/CharacterBaseLoadAnimation.cs @@ -26,7 +26,8 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, + !HookOverrides.Instance.Animation.CharacterBaseLoadAnimation); } public delegate void Delegate(DrawObject* drawBase); diff --git a/Penumbra/Interop/Hooks/Animation/Dismount.cs b/Penumbra/Interop/Hooks/Animation/Dismount.cs index 034011e7..17151083 100644 --- a/Penumbra/Interop/Hooks/Animation/Dismount.cs +++ b/Penumbra/Interop/Hooks/Animation/Dismount.cs @@ -16,7 +16,7 @@ public sealed unsafe class Dismount : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Dismount", Sigs.Dismount, Detour, !HookOverrides.Instance.Animation.Dismount); } public delegate void Delegate(MountContainer* a1, nint a2); diff --git a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs index 48dc0078..29afd4ea 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs @@ -17,10 +17,10 @@ public sealed unsafe class LoadAreaVfx : FastHook public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler) { - _state = state; + _state = state; _collectionResolver = collectionResolver; - _crashHandler = crashHandler; - Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, HookSettings.VfxIdentificationHooks); + _crashHandler = crashHandler; + Task = hooks.CreateHook("Load Area VFX", Sigs.LoadAreaVfx, Detour, !HookOverrides.Instance.Animation.LoadAreaVfx); } public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs index 8d1096d2..91b70ede 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs @@ -20,7 +20,7 @@ public sealed unsafe class LoadCharacterSound : FastHook("Load Character Sound", (nint)VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour, - HookSettings.VfxIdentificationHooks); + !HookOverrides.Instance.Animation.LoadCharacterSound); } public delegate nint Delegate(VfxContainer* container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7); diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index af801345..9a57ca12 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -26,7 +26,7 @@ public sealed unsafe class LoadCharacterVfx : FastHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Load Character VFX", Sigs.LoadCharacterVfx, Detour, !HookOverrides.Instance.Animation.LoadCharacterVfx); } public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4); diff --git a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs index 8bb14db6..cdd82b95 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs @@ -31,7 +31,7 @@ public sealed unsafe class LoadTimelineResources : FastHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, !HookOverrides.Instance.Animation.LoadTimelineResources); } public delegate ulong Delegate(SchedulerTimeline* timeline); diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs index e4a8c83c..858357c8 100644 --- a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -14,7 +14,7 @@ public sealed unsafe class PlayFootstep : FastHook { _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, !HookOverrides.Instance.Animation.PlayFootstep); } public delegate void Delegate(GameObject* gameObject, int id, int unk); diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs index 645b3565..dfbc615a 100644 --- a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -23,7 +23,7 @@ public sealed unsafe class ScheduleClipUpdate : FastHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, !HookOverrides.Instance.Animation.ScheduleClipUpdate); } public delegate void Delegate(ClipScheduler* x); diff --git a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs index 1f3c0e3b..e1751261 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs @@ -20,7 +20,7 @@ public sealed unsafe class SomeActionLoad : FastHook _state = state; _collectionResolver = collectionResolver; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some Action Load", Sigs.LoadSomeAction, Detour, !HookOverrides.Instance.Animation.SomeActionLoad); } public delegate void Delegate(TimelineContainer* timelineManager); diff --git a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs index f2b48afe..75f1240a 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeMountAnimation : FastHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some Mount Animation", Sigs.UnkMountAnimation, Detour, !HookOverrides.Instance.Animation.SomeMountAnimation); } public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3); diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index 8f952df5..7339c397 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -22,7 +22,7 @@ public sealed unsafe class SomePapLoad : FastHook _collectionResolver = collectionResolver; _objects = objects; _crashHandler = crashHandler; - Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some PAP Load", Sigs.LoadSomePap, Detour, !HookOverrides.Instance.Animation.SomePapLoad); } public delegate void Delegate(nint a1, int a2, nint a3, int a4); diff --git a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs index 165bd5eb..9df8d4eb 100644 --- a/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs +++ b/Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs @@ -15,7 +15,7 @@ public sealed unsafe class SomeParasolAnimation : FastHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, HookSettings.VfxIdentificationHooks); + Task = hooks.CreateHook("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, !HookOverrides.Instance.Animation.SomeParasolAnimation); } public delegate void Delegate(DrawObject* drawObject, int unk1); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 9c60096f..a4f4201f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -1,14 +1,140 @@ +using Dalamud.Plugin; +using Newtonsoft.Json; + namespace Penumbra.Interop.Hooks; -public static class HookSettings +public class HookOverrides { - public const bool AllHooks = true; + public static HookOverrides Instance = new(); - public const bool ObjectHooks = true && AllHooks; - public const bool ReplacementHooks = true && AllHooks; - public const bool ResourceHooks = true && AllHooks; - public const bool MetaEntryHooks = true && AllHooks; - public const bool MetaParentHooks = true && AllHooks; - public const bool VfxIdentificationHooks = true && AllHooks; - public const bool PostProcessingHooks = true && AllHooks; + public AnimationHooks Animation; + public MetaHooks Meta; + public ObjectHooks Objects; + public PostProcessingHooks PostProcessing; + public ResourceLoadingHooks ResourceLoading; + public ResourceHooks Resources; + + public HookOverrides Clone() + => new() + { + Animation = Animation, + Meta = Meta, + Objects = Objects, + PostProcessing = PostProcessing, + ResourceLoading = ResourceLoading, + Resources = Resources, + }; + + public struct AnimationHooks + { + public bool ApricotListenerSoundPlayCaller; + public bool CharacterBaseLoadAnimation; + public bool Dismount; + public bool LoadAreaVfx; + public bool LoadCharacterSound; + public bool LoadCharacterVfx; + public bool LoadTimelineResources; + public bool PlayFootstep; + public bool ScheduleClipUpdate; + public bool SomeActionLoad; + public bool SomeMountAnimation; + public bool SomePapLoad; + public bool SomeParasolAnimation; + } + + public struct MetaHooks + { + public bool CalculateHeight; + public bool ChangeCustomize; + public bool EqdpAccessoryHook; + public bool EqdpEquipHook; + public bool EqpHook; + public bool EstHook; + public bool GmpHook; + public bool ModelLoadComplete; + public bool RspBustHook; + public bool RspHeightHook; + public bool RspSetupCharacter; + public bool RspTailHook; + public bool SetupVisor; + public bool UpdateModel; + public bool UpdateRender; + } + + public struct ObjectHooks + { + public bool CharacterBaseDestructor; + public bool CharacterDestructor; + public bool CopyCharacter; + public bool CreateCharacterBase; + public bool EnableDraw; + public bool WeaponReload; + } + + public struct PostProcessingHooks + { + public bool HumanSetupScaling; + public bool HumanCreateDeformer; + public bool HumanOnRenderMaterial; + public bool ModelRendererOnRenderMaterial; + } + + public struct ResourceLoadingHooks + { + public bool CreateFileWHook; + public bool PapHooks; + public bool ReadSqPack; + public bool IncRef; + public bool DecRef; + public bool GetResourceSync; + public bool GetResourceAsync; + public bool CheckFileState; + public bool TexResourceHandleOnLoad; + public bool LoadMdlFileExtern; + } + + public struct ResourceHooks + { + public bool ApricotResourceLoad; + public bool LoadMtrlShpk; + public bool LoadMtrlTex; + public bool ResolvePathHooks; + public bool ResourceHandleDestructor; + } + + public const string FileName = "HookOverrides.json"; + + public static HookOverrides LoadFile(IDalamudPluginInterface pi) + { + var path = Path.Combine(pi.GetPluginConfigDirectory(), FileName); + if (!File.Exists(path)) + return new HookOverrides(); + + try + { + var text = File.ReadAllText(path); + var ret = JsonConvert.DeserializeObject(text); + Penumbra.Log.Warning("A hook override file was loaded, some hooks may be disabled and Penumbra might not be working as expected."); + return ret; + } + catch (Exception ex) + { + Penumbra.Log.Error($"A hook override file was found at {path}, but could not be loaded:\n{ex}"); + return new HookOverrides(); + } + } + + public void Write(IDalamudPluginInterface pi) + { + var path = Path.Combine(pi.GetPluginConfigDirectory(), FileName); + try + { + var text = JsonConvert.SerializeObject(this, Formatting.Indented); + File.WriteAllText(path, text); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not write hook override file to {path}:\n{ex}"); + } + } } diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index aab64871..e71d07dd 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -14,7 +14,7 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); } public delegate ulong Delegate(Character* character); diff --git a/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs b/Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs index f69e98e7..368845b4 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, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize); } public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment); diff --git a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs index 63cca53f..43328600 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpAccessoryHook.cs @@ -16,8 +16,10 @@ public unsafe class EqdpAccessoryHook : FastHook, ID public EqdpAccessoryHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetEqdpAccessoryEntry", Sigs.GetEqdpAccessoryEntry, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EqdpAccessoryHook); + if (!HookOverrides.Instance.Meta.EqdpAccessoryHook) + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) diff --git a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs index 5d5d2f84..fa0d5a29 100644 --- a/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqdpEquipHook.cs @@ -16,8 +16,9 @@ public unsafe class EqdpEquipHook : FastHook, IDisposabl public EqdpEquipHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetEqdpEquipEntry", Sigs.GetEqdpEquipEntry, Detour, metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EqdpEquipHook); + if (!HookOverrides.Instance.Meta.EqdpEquipHook) + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqdpEntry* entry, uint setId, uint raceCode) diff --git a/Penumbra/Interop/Hooks/Meta/EqpHook.cs b/Penumbra/Interop/Hooks/Meta/EqpHook.cs index f47db795..f35b922b 100644 --- a/Penumbra/Interop/Hooks/Meta/EqpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EqpHook.cs @@ -15,8 +15,10 @@ public unsafe class EqpHook : FastHook, IDisposable public EqpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetEqpFlags", Sigs.GetEqpEntry, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EqpHook); + if (!HookOverrides.Instance.Meta.EqpHook) + _metaState.Config.ModsEnabled += Toggle; } private void Detour(CharacterUtility* utility, EqpEntry* flags, CharacterArmor* armor) diff --git a/Penumbra/Interop/Hooks/Meta/EstHook.cs b/Penumbra/Interop/Hooks/Meta/EstHook.cs index 825b1244..8284eb69 100644 --- a/Penumbra/Interop/Hooks/Meta/EstHook.cs +++ b/Penumbra/Interop/Hooks/Meta/EstHook.cs @@ -21,8 +21,9 @@ public unsafe class EstHook : FastHook, IDisposable _metaState = metaState; _characterUtility = characterUtility; Task = hooks.CreateHook("FindEstEntry", Sigs.FindEstEntry, Detour, - metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.EstHook); + if (!HookOverrides.Instance.Meta.EstHook) + _metaState.Config.ModsEnabled += Toggle; } private EstEntry Detour(ResourceHandle* estResource, uint genderRace, uint id) diff --git a/Penumbra/Interop/Hooks/Meta/GmpHook.cs b/Penumbra/Interop/Hooks/Meta/GmpHook.cs index 12b221d9..d656ebdb 100644 --- a/Penumbra/Interop/Hooks/Meta/GmpHook.cs +++ b/Penumbra/Interop/Hooks/Meta/GmpHook.cs @@ -17,8 +17,10 @@ public unsafe class GmpHook : FastHook, IDisposable public GmpHook(HookManager hooks, MetaState metaState) { _metaState = metaState; - Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetGmpEntry", Sigs.GetGmpEntry, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.GmpHook); + if (!HookOverrides.Instance.Meta.GmpHook) + _metaState.Config.ModsEnabled += Toggle; } private ulong Detour(CharacterUtility* characterUtility, ulong* outputEntry, ushort setId) diff --git a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs index c1803745..4b9b05b1 100644 --- a/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs +++ b/Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs @@ -13,7 +13,7 @@ public sealed unsafe class ModelLoadComplete : FastHook("Model Load Complete", vtables.HumanVTable[59], Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Model Load Complete", vtables.HumanVTable[59], Detour, !HookOverrides.Instance.Meta.ModelLoadComplete); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs index e08dc393..c49556bf 100644 --- a/Penumbra/Interop/Hooks/Meta/RspBustHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspBustHook.cs @@ -19,8 +19,10 @@ public unsafe class RspBustHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetRspBust", Sigs.GetRspBust, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.RspBustHook); + if (!HookOverrides.Instance.Meta.RspBustHook) + _metaState.Config.ModsEnabled += Toggle; } private float* Detour(nint cmpResource, float* storage, SubRace clan, byte gender, byte bodyType, byte bustSize) @@ -34,7 +36,9 @@ public unsafe class RspBustHook : FastHook, IDisposable } var ret = storage; - if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 + && _metaState.RspCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var bustScale = bustSize / 100f; var ptr = CmpFile.GetDefaults(_metaFileManager, clan, RspAttribute.BustMinX); diff --git a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs index 20e3c939..49180d6e 100644 --- a/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspHeightHook.cs @@ -17,10 +17,12 @@ public class RspHeightHook : FastHook, IDisposable public RspHeightHook(HookManager hooks, MetaState metaState, MetaFileManager metaFileManager) { - _metaState = metaState; + _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetRspHeight", Sigs.GetRspHeight, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.RspHeightHook); + if (!HookOverrides.Instance.Meta.RspHeightHook) + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, SubRace clan, byte gender, byte bodyType, byte height) @@ -33,6 +35,7 @@ public class RspHeightHook : FastHook, IDisposable // Special cases. if (height == 0xFF) return 1.0f; + if (height > 100) height = 0; diff --git a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs index 8bcc7593..952a2e29 100644 --- a/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs +++ b/Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class RspSetupCharacter : FastHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("RSP Setup Character", Sigs.RspSetupCharacter, Detour, !HookOverrides.Instance.Meta.RspSetupCharacter); } public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5); diff --git a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs index 86d21c6f..b434efa6 100644 --- a/Penumbra/Interop/Hooks/Meta/RspTailHook.cs +++ b/Penumbra/Interop/Hooks/Meta/RspTailHook.cs @@ -19,14 +19,18 @@ public class RspTailHook : FastHook, IDisposable { _metaState = metaState; _metaFileManager = metaFileManager; - Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, metaState.Config.EnableMods && HookSettings.MetaEntryHooks); - _metaState.Config.ModsEnabled += Toggle; + Task = hooks.CreateHook("GetRspTail", Sigs.GetRspTail, Detour, + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.RspTailHook); + if (!HookOverrides.Instance.Meta.RspTailHook) + _metaState.Config.ModsEnabled += Toggle; } private unsafe float Detour(nint cmpResource, Race race, byte gender, byte isSecondSubRace, byte bodyType, byte tailLength) { float scale; - if (bodyType < 2 && _metaState.RspCollection.TryPeek(out var collection) && collection is { Valid: true, ModCollection.MetaCache: { } cache }) + if (bodyType < 2 + && _metaState.RspCollection.TryPeek(out var collection) + && collection is { Valid: true, ModCollection.MetaCache: { } cache }) { var clan = (SubRace)(((int)race - 1) * 2 + 1 + isSecondSubRace); var (minIdent, maxIdent) = gender == 0 diff --git a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs index 83c0e0c4..063a9462 100644 --- a/Penumbra/Interop/Hooks/Meta/SetupVisor.cs +++ b/Penumbra/Interop/Hooks/Meta/SetupVisor.cs @@ -19,7 +19,7 @@ public sealed unsafe class SetupVisor : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Setup Visor", Sigs.SetupVisor, Detour, !HookOverrides.Instance.Meta.SetupVisor); } public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index a088a0f2..72beea0e 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -15,7 +15,7 @@ public sealed unsafe class UpdateModel : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, HookSettings.MetaParentHooks); + Task = hooks.CreateHook("Update Model", Sigs.UpdateModel, Detour, !HookOverrides.Instance.Meta.UpdateModel); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Meta/UpdateRender.cs b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs index 95cc0e15..ef0068b6 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateRender.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateRender.cs @@ -13,8 +13,8 @@ public sealed unsafe class UpdateRender : FastHook public UpdateRender(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vTables) { _collectionResolver = collectionResolver; - _metaState = metaState; - Task = hooks.CreateHook("Human.UpdateRender", vTables.HumanVTable[4], Detour, HookSettings.MetaParentHooks); + _metaState = metaState; + Task = hooks.CreateHook("Human.UpdateRender", vTables.HumanVTable[4], Detour, !HookOverrides.Instance.Meta.UpdateRender); } public delegate void Delegate(DrawObject* drawObject); diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index c67bb9f3..7636718e 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.CharacterBaseDestructor); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index 618d0bd7..ffe2f72d 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -19,7 +19,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Sigs.CharacterDestructor, Detour, !HookOverrides.Instance.Objects.CharacterDestructor); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs index d81043c8..bc18a7ad 100644 --- a/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs +++ b/Penumbra/Interop/Hooks/Objects/CopyCharacter.cs @@ -15,7 +15,7 @@ public sealed unsafe class CopyCharacter : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.CopyCharacter); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs index f00a9984..e29876ac 100644 --- a/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs +++ b/Penumbra/Interop/Hooks/Objects/CreateCharacterBase.cs @@ -16,7 +16,7 @@ public sealed unsafe class CreateCharacterBase : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.CreateCharacterBase); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 8b701fe5..68bb28af 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -17,7 +17,7 @@ public sealed unsafe class EnableDraw : IHookService public EnableDraw(HookManager hooks, GameState state) { _state = state; - _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, HookSettings.ObjectHooks); + _task = hooks.CreateHook("Enable Draw", Sigs.EnableDraw, Detour, !HookOverrides.Instance.Objects.EnableDraw); } private delegate void Delegate(GameObject* gameObject); diff --git a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs index da31840f..b09103f6 100644 --- a/Penumbra/Interop/Hooks/Objects/WeaponReload.cs +++ b/Penumbra/Interop/Hooks/Objects/WeaponReload.cs @@ -16,7 +16,7 @@ public sealed unsafe class WeaponReload : EventWrapperPtr _task = hooks.CreateHook(Name, Address, Detour, HookSettings.ObjectHooks); + => _task = hooks.CreateHook(Name, Address, Detour, !HookOverrides.Instance.Objects.WeaponReload); private readonly Task> _task; diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 834a7d28..1aa09d7f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -38,9 +38,9 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi _resourceLoader = resourceLoader; _framework = framework; _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, - HookSettings.PostProcessingHooks).Result; + !HookOverrides.Instance.PostProcessing.HumanSetupScaling).Result; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], - CreateDeformer, HookSettings.PostProcessingHooks).Result; + CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } public void Dispose() diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index b87d33ef..53b69741 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -90,20 +90,26 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRenderer = modelRenderer; _communicator = communicator; - _skinState = new( + _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); - _irisState = new(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); - _characterGlassState = new(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new(() => _modelRenderer.CharacterTransparencyShaderPackage, () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new(() => _modelRenderer.CharacterTattooShaderPackage, () => _modelRenderer.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new(() => _modelRenderer.CharacterOcclusionShaderPackage, () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); - _hairMaskState = new(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, + () => _modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTransparencyShaderPackage, + () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTattooShaderPackage, + () => _modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => _modelRenderer.CharacterOcclusionShaderPackage, + () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + _hairMaskState = + new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], - OnRenderHumanMaterial, HookSettings.PostProcessingHooks).Result; + OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", - Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, HookSettings.PostProcessingHooks).Result; + Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, + !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } @@ -123,7 +129,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _skinState.ClearMaterials(); } - public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() + public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong + HairMask) GetAndResetSlowPathCallDeltas() => (_skinState.GetAndResetSlowPathCallDelta(), _irisState.GetAndResetSlowPathCallDelta(), _characterGlassState.GetAndResetSlowPathCallDelta(), @@ -208,7 +215,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForModelRenderer() - => _irisState.MaterialCount + _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + => _irisState.MaterialCount + + _characterGlassState.MaterialCount + + _characterTransparencyState.MaterialCount + + _characterTattooState.MaterialCount + + _characterOcclusionState.MaterialCount + + _hairMaskState.MaterialCount; private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs index 8d0ac8cb..a9a5f41d 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/CreateFileWHook.cs @@ -19,7 +19,7 @@ public unsafe class CreateFileWHook : IDisposable, IRequiredService public CreateFileWHook(IGameInteropProvider interop) { _createFileWHook = interop.HookFromImport(null, "KERNEL32.dll", "CreateFileW", 0, CreateFileWDetour); - if (HookSettings.ReplacementHooks) + if (!HookOverrides.Instance.ResourceLoading.CreateFileWHook) _createFileWHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs index 199525fb..d8801b81 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/FileReadService.cs @@ -15,7 +15,7 @@ public unsafe class FileReadService : IDisposable, IRequiredService _resourceManager = resourceManager; _performance = performance; interop.InitializeFromAttributes(this); - if (HookSettings.ReplacementHooks) + if (!HookOverrides.Instance.ResourceLoading.ReadSqPack) _readSqPackHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs index 81712cca..de0014d2 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/MappedCodeReader.cs @@ -4,10 +4,11 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public class MappedCodeReader(UnmanagedMemoryAccessor data, long offset) : CodeReader { - public override int ReadByte() { - if (offset >= data.Capacity) - return -1; + public override int ReadByte() + { + if (offset >= data.Capacity) + return -1; - return data.ReadByte(offset++); - } + return data.ReadByte(offset++); + } } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index ea12a480..5ba8c975 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -8,6 +8,9 @@ public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResour public void Enable() { + if (HookOverrides.Instance.ResourceLoading.PapHooks) + return; + ReadOnlySpan<(string Sig, string Name)> signatures = [ (Sigs.LoadAlwaysResidentMotionPacks, nameof(Sigs.LoadAlwaysResidentMotionPacks)), diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs index f5dd2d45..4be0da00 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -14,7 +14,6 @@ public unsafe class PeSigScanner : IDisposable private readonly nint _moduleBaseAddress; private readonly uint _textSectionVirtualAddress; - public PeSigScanner() { var mainModule = Process.GetCurrentProcess().MainModule!; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 8b99dc37..f75b0623 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -30,13 +30,14 @@ public unsafe class ResourceService : IDisposable, IRequiredService _decRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); - if (HookSettings.ReplacementHooks) - { + if (HookOverrides.Instance.ResourceLoading.GetResourceSync) _getResourceSyncHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.DecRef) _decRefHook.Enable(); - } } public ResourceHandle* GetResource(ResourceCategory category, ResourceType type, CiByteString path) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 80ba5cb9..fc3289bd 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -46,12 +46,12 @@ public unsafe class TexMdlService : IDisposable, IRequiredService { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); - if (HookSettings.ReplacementHooks) - { + if (HookOverrides.Instance.ResourceLoading.CheckFileState) _checkFileStateHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.LoadMdlFileExtern) _loadMdlFileExternHook.Enable(); + if (HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); - } } /// Add CRC64 if the given file is a model or texture file and has an associated path. diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs index 511e842f..f6cccc19 100644 --- a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -11,7 +11,7 @@ public sealed unsafe class ApricotResourceLoad : FastHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookSettings.ResourceHooks); + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookOverrides.Instance.Resources.ApricotResourceLoad); } public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs index 8447762b..7aaa62d5 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -14,7 +14,7 @@ public sealed unsafe class LoadMtrlShpk : FastHook { _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookSettings.ResourceHooks); + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookOverrides.Instance.Resources.LoadMtrlShpk); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index 7bc3c7b0..ed0e067b 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -11,7 +11,7 @@ public sealed unsafe class LoadMtrlTex : FastHook public LoadMtrlTex(HookManager hooks, GameState gameState) { _gameState = gameState; - Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookSettings.ResourceHooks); + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookOverrides.Instance.Resources.LoadMtrlTex); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index e1b6e46e..66945009 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -67,7 +67,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable // @formatter:on - if (HookSettings.ResourceHooks) + if (HookOverrides.Instance.Resources.ResolvePathHooks) Enable(); } diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index cd4a53c4..5c4b5c90 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -23,7 +23,8 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, HookSettings.ResourceHooks); + => _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, + HookOverrides.Instance.Resources.ResourceHandleDestructor); private readonly Task> _task; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 5f8d6805..8ea74987 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra; @@ -52,9 +53,10 @@ public class Penumbra : IDalamudPlugin { try { - _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); - Messager = _services.GetService(); - _validityChecker = _services.GetService(); + HookOverrides.Instance = HookOverrides.LoadFile(pluginInterface); + _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); + Messager = _services.GetService(); + _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); var startup = _services.GetService() @@ -215,6 +217,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\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")}\n"); sb.Append( diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index a1e9da03..3a64e556 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -97,6 +97,7 @@ public class DebugTab : Window, ITab, IUiService private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; + private readonly HookOverrideDrawer _hookOverrides; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, @@ -106,7 +107,8 @@ public class DebugTab : Window, ITab, IUiService DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, - Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer) + Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, + HookOverrideDrawer hookOverrides) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -143,6 +145,7 @@ public class DebugTab : Window, ITab, IUiService _ipcTester = ipcTester; _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; + _hookOverrides = hookOverrides; _objects = objects; _clientState = clientState; } @@ -166,34 +169,21 @@ public class DebugTab : Window, ITab, IUiService return; DrawDebugTabGeneral(); - ImGui.NewLine(); _crashHandlerPanel.Draw(); - ImGui.NewLine(); _diagnostics.DrawDiagnostics(); DrawPerformanceTab(); - ImGui.NewLine(); DrawPathResolverDebug(); - ImGui.NewLine(); DrawActorsDebug(); - ImGui.NewLine(); DrawCollectionCaches(); - ImGui.NewLine(); _texHeaderDrawer.Draw(); - ImGui.NewLine(); DrawDebugCharacterUtility(); - ImGui.NewLine(); DrawShaderReplacementFixer(); - ImGui.NewLine(); DrawData(); - ImGui.NewLine(); DrawResourceProblems(); - ImGui.NewLine(); + _hookOverrides.Draw(); DrawPlayerModelInfo(); - ImGui.NewLine(); DrawGlobalVariableInfo(); - ImGui.NewLine(); DrawDebugTabIpc(); - ImGui.NewLine(); } @@ -434,7 +424,6 @@ public class DebugTab : Window, ITab, IUiService private void DrawPerformanceTab() { - ImGui.NewLine(); if (!ImGui.CollapsingHeader("Performance")) return; diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs new file mode 100644 index 00000000..7af1f884 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -0,0 +1,63 @@ +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Hooks; + +namespace Penumbra.UI.Tabs.Debug; + +public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : IUiService +{ + private HookOverrides? _overrides; + + public void Draw() + { + using var header = ImUtf8.CollapsingHeaderId("Generate Hook Override"u8); + if (!header) + return; + + _overrides ??= HookOverrides.Instance.Clone(); + + if (ImUtf8.Button("Save"u8)) + _overrides.Write(pluginInterface); + + ImGui.SameLine(); + var path = Path.Combine(pluginInterface.GetPluginConfigDirectory(), HookOverrides.FileName); + var exists = File.Exists(path); + if (ImUtf8.ButtonEx("Delete"u8, disabled: !exists, tooltip: exists ? ""u8 : "File does not exist."u8)) + try + { + File.Delete(path); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not delete hook override file at {path}:\n{ex}"); + } + + bool? all = null; + ImGui.SameLine(); + if (ImUtf8.Button("Disable All Hooks"u8)) + all = true; + ImGui.SameLine(); + if (ImUtf8.Button("Enable All Hooks"u8)) + all = false; + + foreach (var propertyField in typeof(HookOverrides).GetFields().Where(f => f is { IsStatic: false, FieldType.IsValueType: true })) + { + using var tree = ImUtf8.TreeNode(propertyField.Name); + if (!tree) + continue; + + var property = propertyField.GetValue(_overrides); + foreach (var valueField in propertyField.FieldType.GetFields()) + { + var value = valueField.GetValue(property) as bool? ?? false; + if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || all.HasValue) + { + valueField.SetValue(property, all ?? value); + propertyField.SetValue(_overrides, property); + } + } + } + } +} From 73b9d1fca0ede105413561ba0d61f17d7b176636 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 16:49:22 +0200 Subject: [PATCH 299/865] Meh. --- Penumbra/Interop/Hooks/HookSettings.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index a4f4201f..0c0a4020 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -5,6 +5,9 @@ namespace Penumbra.Interop.Hooks; public class HookOverrides { + [JsonIgnore] + public bool IsCustomLoaded { get; private set; } + public static HookOverrides Instance = new(); public AnimationHooks Animation; @@ -113,7 +116,8 @@ public class HookOverrides try { var text = File.ReadAllText(path); - var ret = JsonConvert.DeserializeObject(text); + var ret = JsonConvert.DeserializeObject(text)!; + ret.IsCustomLoaded = true; Penumbra.Log.Warning("A hook override file was loaded, some hooks may be disabled and Penumbra might not be working as expected."); return ret; } From 7579eaacbe0ffd3fcfcd03e33c6b526d5f2d2310 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 17:14:28 +0200 Subject: [PATCH 300/865] Meh. --- Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs | 8 ++++---- Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs | 6 +++--- Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs | 3 ++- Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs | 4 ++-- Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs | 2 +- Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs | 2 +- .../Interop/Hooks/Resources/ResourceHandleDestructor.cs | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index f75b0623..e55c9bb0 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -30,13 +30,13 @@ public unsafe class ResourceService : IDisposable, IRequiredService _decRefHook = interop.HookFromAddress( (nint)CSResourceHandle.MemberFunctionPointers.DecRef, ResourceHandleDecRefDetour); - if (HookOverrides.Instance.ResourceLoading.GetResourceSync) + if (!HookOverrides.Instance.ResourceLoading.GetResourceSync) _getResourceSyncHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.GetResourceAsync) + if (!HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.IncRef) + if (!HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.DecRef) + if (!HookOverrides.Instance.ResourceLoading.DecRef) _decRefHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index fc3289bd..d4a2dfba 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -46,11 +46,11 @@ public unsafe class TexMdlService : IDisposable, IRequiredService { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); - if (HookOverrides.Instance.ResourceLoading.CheckFileState) + if (!HookOverrides.Instance.ResourceLoading.CheckFileState) _checkFileStateHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.LoadMdlFileExtern) + if (!HookOverrides.Instance.ResourceLoading.LoadMdlFileExtern) _loadMdlFileExternHook.Enable(); - if (HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) + if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); } diff --git a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs index f6cccc19..40860b0b 100644 --- a/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs +++ b/Penumbra/Interop/Hooks/Resources/ApricotResourceLoad.cs @@ -11,7 +11,8 @@ public sealed unsafe class ApricotResourceLoad : FastHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, HookOverrides.Instance.Resources.ApricotResourceLoad); + Task = hooks.CreateHook("Load Apricot Resource", Sigs.ApricotResourceLoad, Detour, + !HookOverrides.Instance.Resources.ApricotResourceLoad); } public delegate byte Delegate(ResourceHandle* handle, nint unk1, byte unk2); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs index 7aaa62d5..8c410ad8 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs @@ -12,9 +12,9 @@ public sealed unsafe class LoadMtrlShpk : FastHook public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator) { - _gameState = gameState; + _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, HookOverrides.Instance.Resources.LoadMtrlShpk); + Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, !HookOverrides.Instance.Resources.LoadMtrlShpk); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index ed0e067b..0759d9b1 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -11,7 +11,7 @@ public sealed unsafe class LoadMtrlTex : FastHook public LoadMtrlTex(HookManager hooks, GameState gameState) { _gameState = gameState; - Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, HookOverrides.Instance.Resources.LoadMtrlTex); + Task = hooks.CreateHook("Load Material Textures", Sigs.LoadMtrlTex, Detour, !HookOverrides.Instance.Resources.LoadMtrlTex); } public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 66945009..b1b23f27 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -67,7 +67,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable // @formatter:on - if (HookOverrides.Instance.Resources.ResolvePathHooks) + if (!HookOverrides.Instance.Resources.ResolvePathHooks) Enable(); } diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index 5c4b5c90..bdb11752 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -24,7 +24,7 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr _task = hooks.CreateHook(Name, Sigs.ResourceHandleDestructor, Detour, - HookOverrides.Instance.Resources.ResourceHandleDestructor); + !HookOverrides.Instance.Resources.ResourceHandleDestructor); private readonly Task> _task; From 1e1637f0e72dff18e6d1beee6c686d5ed509b9f8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 17:14:46 +0200 Subject: [PATCH 301/865] Test IMC group toggling off. --- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- .../Manager/OptionEditor/ImcAttributeCache.cs | 34 +++++------------- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 36 +++++++++++++++---- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 03896134..5f99673e 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -242,7 +242,7 @@ public class ImcModGroup(Mod mod) : IModGroup continue; var option = OptionData[i]; - mask |= option.AttributeMask; + mask ^= option.AttributeMask; } return mask; diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs index e1235c5b..a7b73ac9 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -21,23 +21,14 @@ public unsafe ref struct ImcAttributeCache _option[i] = byte.MaxValue; var flag = (ushort)(1 << i); - var set = (group.DefaultEntry.AttributeMask & flag) != 0; - if (set) - { - _canChange[i] = true; - _option[i] = byte.MaxValue - 1; - continue; - } - foreach (var (option, idx) in group.OptionData.WithIndex()) { - set = (option.AttributeMask & flag) != 0; - if (set) - { - _canChange[i] = option.AttributeMask != flag; - _option[i] = (byte)idx; - break; - } + if ((option.AttributeMask & flag) == 0) + continue; + + _canChange[i] = option.AttributeMask != flag; + _option[i] = (byte)idx; + break; } if (_option[i] == byte.MaxValue && LowestUnsetMask is 0) @@ -65,25 +56,16 @@ public unsafe ref struct ImcAttributeCache return true; } - if (!_canChange[idx]) - return false; - var mask = (ushort)(oldMask | flag); if (oldMask == mask) return false; group.DefaultEntry = group.DefaultEntry with { AttributeMask = mask }; - if (_option[idx] <= ImcEntry.NumAttributes) - { - var option = group.OptionData[_option[idx]]; - option.AttributeMask = (ushort)(option.AttributeMask & ~flag); - } - return true; } /// Set an attribute flag to a value if possible, remove it from its prior option or the default entry if necessary, and return if anything changed. - public readonly bool Set(ImcSubMod option, int idx, bool value) + public readonly bool Set(ImcSubMod option, int idx, bool value, bool turnOffDefault = false) { if (!_canChange[idx]) return false; @@ -110,7 +92,7 @@ public unsafe ref struct ImcAttributeCache var oldOption = option.Group.OptionData[_option[idx]]; oldOption.AttributeMask = (ushort)(oldOption.AttributeMask & ~flag); } - else if (_option[idx] is byte.MaxValue - 1) + else if (turnOffDefault && _option[idx] is byte.MaxValue - 1) { option.Group.DefaultEntry = option.Group.DefaultEntry with { diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index bbb5e54e..9d1ab78a 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -3,6 +3,9 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; +using OtterGui.Text.Widget; +using OtterGui.Widgets; +using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; @@ -86,7 +89,8 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr foreach (var (option, idx) in group.OptionData.WithIndex().Where(o => !o.Value.IsDisableSubMod)) { using var id = ImUtf8.PushId(idx); - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option, + group.DefaultEntry.AttributeMask); } } } @@ -132,15 +136,18 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr } } - private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data, + ushort? defaultMask = null) { for (var i = 0; i < ImcEntry.NumAttributes; ++i) { - using var id = ImRaii.PushId(i); - var value = (mask & (1 << i)) != 0; - using (ImRaii.Disabled(!cache.CanChange(i))) + using var id = ImRaii.PushId(i); + var flag = 1 << i; + var value = (mask & flag) != 0; + var inDefault = defaultMask.HasValue && (defaultMask & flag) != 0; + using (ImRaii.Disabled(defaultMask != null && !cache.CanChange(i))) { - if (ImUtf8.Checkbox(""u8, ref value)) + if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : ImUtf8.Checkbox(""u8, ref value)) { if (data is ImcModGroup g) editor.ChangeDefaultAttribute(g, cache, i, value); @@ -154,4 +161,21 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr ImUtf8.SameLineInner(); } } + + private sealed class NegativeCheckbox : MultiStateCheckbox + { + public static readonly NegativeCheckbox Instance = new(); + + protected override void RenderSymbol(bool value, Vector2 position, float size) + { + if (value) + SymbolHelpers.RenderCross(ImGui.GetWindowDrawList(), position, ImGui.GetColorU32(ImGuiCol.CheckMark), size); + } + + protected override bool NextValue(bool value) + => !value; + + protected override bool PreviousValue(bool value) + => !value; + } } From 5e9c7f7eac0c6334b3063918bd221a1525e80844 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 22:17:56 +0200 Subject: [PATCH 302/865] Fix IMC import sanity check. --- OtterGui | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 2b79faac..c53955cb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 2b79faacff30a31e9ad4b0a3c5d57ffd6e34cfa4 +Subproject commit c53955cb6199dd418c5a9538d3251ac5942e7067 diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 5f99673e..7b0eb094 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -186,7 +186,7 @@ public class ImcModGroup(Mod mod) : IModGroup return null; } - var rollingMask = ret.DefaultEntry.AttributeMask; + var rollingMask = 0ul; if (options != null) foreach (var child in options.Children()) { From d903f1b8c3a61fdadc9387cad59cf7a3f337ab64 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 1 Aug 2024 22:18:26 +0200 Subject: [PATCH 303/865] Update GetResourceSync and GetResourceAsync function signatures for testing, including unused stack parameters. --- .../Hooks/ResourceLoading/ResourceService.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index e55c9bb0..126505d1 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -44,7 +44,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService { var hash = path.Crc32; return GetResourceHandler(true, (ResourceManager*)_resourceManager.ResourceManagerAddress, - &category, &type, &hash, path.Path, null, false); + &category, &type, &hash, path.Path, null, 0, 0, 0); } public SafeResourceHandle GetSafeResource(ResourceCategory category, ResourceType type, CiByteString path) @@ -76,10 +76,10 @@ public unsafe class ResourceService : IDisposable, IRequiredService public event GetResourcePreDelegate? ResourceRequested; private delegate ResourceHandle* GetResourceSyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId, - ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams); + 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, bool isUnknown); + 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!; @@ -88,27 +88,28 @@ public unsafe class ResourceService : IDisposable, IRequiredService private readonly Hook _getResourceAsyncHook = null!; private ResourceHandle* GetResourceSyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams) - => GetResourceHandler(true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, false); + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, nint unk8, uint unk9) + => GetResourceHandler(true, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, 0, unk8, unk9); private ResourceHandle* GetResourceAsyncDetour(ResourceManager* resourceManager, ResourceCategory* categoryId, ResourceType* resourceType, - int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) - => GetResourceHandler(false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + int* resourceHash, byte* path, GetResourceParameters* pGetResParams, byte isUnk, nint unk8, uint unk9) + => GetResourceHandler(false, resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk, unk8, unk9); /// /// Resources can be obtained synchronously and asynchronously. We need to change behaviour in both cases. /// Both work basically the same, so we can reduce the main work to one function used by both hooks. /// private ResourceHandle* GetResourceHandler(bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, - ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, bool isUnk) + ResourceType* resourceType, int* resourceHash, byte* path, GetResourceParameters* pGetResParams, byte isUnk, nint unk8, uint unk9) { using var performance = _performance.Measure(PerformanceType.GetResourceHandler); if (!Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) { Penumbra.Log.Error("[ResourceService] Could not create GamePath from resource path."); return isSync - ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams) - : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk); + ? _getResourceSyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, unk8, unk9) + : _getResourceAsyncHook.Original(resourceManager, categoryId, resourceType, resourceHash, path, pGetResParams, isUnk, unk8, + unk9); } ResourceHandle* returnValue = null; @@ -117,17 +118,17 @@ public unsafe class ResourceService : IDisposable, IRequiredService if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9); } /// Call the original GetResource function. public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, - GetResourceParameters* resourceParameters = null, bool unk = false) + GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) => sync ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters) + resourceParameters, unk8, unk9) : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk); + resourceParameters, unk, unk8, unk9); #endregion From f3e72711578f865e6c94fa68f69d50aa75248d90 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 2 Aug 2024 12:48:57 +0000 Subject: [PATCH 304/865] [CI] Updating repo.json for testing_1.2.0.18 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index adf152b0..e1653454 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.17", + "TestingAssemblyVersion": "1.2.0.18", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.17/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.18/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6241187431f313b6ac9d1959943677d04752546c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Aug 2024 15:15:19 +0200 Subject: [PATCH 305/865] Fix span constructors going over boundaries for ByteStrings. --- Penumbra.String | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.String b/Penumbra.String index 91f0f211..bd52d080 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 91f0f21137c61bd39281debf88a8ecc494043330 +Subproject commit bd52d080b72d67263dc47068e461f17c93bdc779 From 4454ac48daadb2375806677f58fe9a7bb5710b8a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 3 Aug 2024 13:18:14 +0000 Subject: [PATCH 306/865] [CI] Updating repo.json for testing_1.2.0.19 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e1653454..cbe2b121 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.18", + "TestingAssemblyVersion": "1.2.0.19", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.18/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.19/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 069b28272bcec59d03fc84e89850e7acf279e180 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:09:20 +0200 Subject: [PATCH 307/865] Add TextureArraySlicer --- .../Interop/Services/TextureArraySlicer.cs | 119 ++++++++++++++++++ Penumbra/Penumbra.csproj | 8 ++ Penumbra/UI/WindowSystem.cs | 31 +++-- 3 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 Penumbra/Interop/Services/TextureArraySlicer.cs diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs new file mode 100644 index 00000000..c934ac2b --- /dev/null +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -0,0 +1,119 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using OtterGui.Services; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; + +namespace Penumbra.Interop.Services; + +/// +/// Creates ImGui handles over slices of array textures, and manages their lifetime. +/// +public sealed unsafe class TextureArraySlicer : IUiService, IDisposable +{ + private const uint InitialTimeToLive = 2; + + private readonly Dictionary<(nint XivTexture, byte SliceIndex), SliceState> _activeSlices = []; + 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) + { + if (texture == null) + throw new ArgumentNullException(nameof(texture)); + if (sliceIndex >= texture->ArraySize) + throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) + { + state.Refresh(); + return (nint)state.ShaderResourceView; + } + var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; + var description = srv.Description; + switch (description.Dimension) + { + case ShaderResourceViewDimension.Texture1D: + case ShaderResourceViewDimension.Texture2D: + case ShaderResourceViewDimension.Texture2DMultisampled: + case ShaderResourceViewDimension.Texture3D: + case ShaderResourceViewDimension.TextureCube: + // This function treats these as single-slice arrays. + // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. + break; + case ShaderResourceViewDimension.Texture1DArray: + description.Texture1DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.Texture2DArray: + description.Texture2DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.Texture2DMultisampledArray: + description.Texture2DMSArray.FirstArraySlice = sliceIndex; + description.Texture2DMSArray.ArraySize = 1; + break; + case ShaderResourceViewDimension.TextureCubeArray: + description.TextureCubeArray.First2DArrayFace = sliceIndex * 6; + description.TextureCubeArray.CubeCount = 1; + break; + default: + throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}"); + } + state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); + _activeSlices.Add(((nint)texture, sliceIndex), state); + return (nint)state.ShaderResourceView; + } + + public void Tick() + { + try + { + foreach (var (key, slice) in _activeSlices) + { + if (!slice.Tick()) + _expiredKeys.Add(key); + } + foreach (var key in _expiredKeys) + { + _activeSlices.Remove(key); + } + } + finally + { + _expiredKeys.Clear(); + } + } + + public void Dispose() + { + foreach (var slice in _activeSlices.Values) + { + slice.Dispose(); + } + } + + private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable + { + public readonly ShaderResourceView ShaderResourceView = shaderResourceView; + + private uint _timeToLive = InitialTimeToLive; + + public void Refresh() + { + _timeToLive = InitialTimeToLive; + } + + public bool Tick() + { + if (unchecked(_timeToLive--) > 0) + return true; + + ShaderResourceView.Dispose(); + return false; + } + + public void Dispose() + { + ShaderResourceView.Dispose(); + } + } +} diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 24ffe469..8e143e3c 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -72,6 +72,14 @@ $(DalamudLibPath)Iced.dll False + + $(DalamudLibPath)SharpDX.dll + False + + + $(DalamudLibPath)SharpDX.Direct3D11.dll + False + lib\OtterTex.dll diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 6d382ad4..575a381f 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using OtterGui.Services; +using Penumbra.Interop.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.UI.Knowledge; using Penumbra.UI.Tabs.Debug; @@ -10,23 +11,25 @@ namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable, IUiService { - private readonly IUiBuilder _uiBuilder; - private readonly WindowSystem _windowSystem; - private readonly FileDialogService _fileDialog; - public readonly ConfigWindow Window; - public readonly PenumbraChangelog Changelog; - public readonly KnowledgeWindow KnowledgeWindow; + private readonly IUiBuilder _uiBuilder; + private readonly WindowSystem _windowSystem; + private readonly FileDialogService _fileDialog; + private readonly TextureArraySlicer _textureArraySlicer; + public readonly ConfigWindow Window; + public readonly PenumbraChangelog Changelog; + public readonly KnowledgeWindow KnowledgeWindow; public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab, - KnowledgeWindow knowledgeWindow) + KnowledgeWindow knowledgeWindow, TextureArraySlicer textureArraySlicer) { - _uiBuilder = pi.UiBuilder; - _fileDialog = fileDialog; - KnowledgeWindow = knowledgeWindow; - Changelog = changelog; - Window = window; - _windowSystem = new WindowSystem("Penumbra"); + _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; + _textureArraySlicer = textureArraySlicer; + KnowledgeWindow = knowledgeWindow; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); @@ -37,6 +40,7 @@ public class PenumbraWindowSystem : IDisposable, IUiService _uiBuilder.OpenConfigUi += Window.OpenSettings; _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.Draw += _fileDialog.Draw; + _uiBuilder.Draw += _textureArraySlicer.Tick; _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; _uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes; _uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden; @@ -51,5 +55,6 @@ public class PenumbraWindowSystem : IDisposable, IUiService _uiBuilder.OpenConfigUi -= Window.OpenSettings; _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.Draw -= _fileDialog.Draw; + _uiBuilder.Draw -= _textureArraySlicer.Tick; } } From 60986c78f8449b2e6aed128b5b2b42ce6aafe35a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:17:48 +0200 Subject: [PATCH 308/865] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 75582ece..ee6c6faa 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c +Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a From 59b3859f117e083cb105e84838ad3d5bd8c186fc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:31:12 +0200 Subject: [PATCH 309/865] Minor upgrades to follow dependencies --- Penumbra/Import/Models/Export/MaterialExporter.cs | 11 ++++++----- Penumbra/Import/Textures/TextureDrawer.cs | 4 +--- Penumbra/Services/MigrationManager.cs | 4 ++-- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 6 +----- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 5df9e1c1..62892473 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -49,7 +49,7 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = new LegacyColorTable(material.Mtrl.Table); + var table = new LegacyColorTable(material.Mtrl.Table!); var normal = material.Textures[TextureUsage.SamplerNormal]; @@ -103,6 +103,7 @@ public class MaterialExporter // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. + // TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore. private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation { public Image Normal { get; } = normal.Clone(); @@ -139,17 +140,17 @@ public class MaterialExporter var nextRow = table[tableRow.Next]; // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp(prevRow.Diffuse, nextRow.Diffuse, tableRow.Weight); + var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight); baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); baseColorSpan[x].A = normalPixel.B; // Specular (table) - var lerpedSpecularColor = Vector3.Lerp(prevRow.Specular, nextRow.Specular, tableRow.Weight); - var lerpedSpecularFactor = float.Lerp(prevRow.SpecularStrength, nextRow.SpecularStrength, tableRow.Weight); + var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight); + var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight); specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); // Emissive (table) - var lerpedEmissive = Vector3.Lerp(prevRow.Emissive, nextRow.Emissive, tableRow.Weight); + var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight); emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); // Normal (.rg) diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index bd95d1ab..c83604e4 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -18,9 +18,7 @@ public static class TextureDrawer { if (texture.TextureWrap != null) { - size = size.X < texture.TextureWrap.Width - ? size with { Y = texture.TextureWrap.Height * size.X / texture.TextureWrap.Width } - : new Vector2(texture.TextureWrap.Width, texture.TextureWrap.Height); + size = texture.TextureWrap.Size.Contain(size); ImGui.Image(texture.TextureWrap.ImGuiHandle, size); DrawData(texture); diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 84318da6..7726f6fd 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -279,7 +279,7 @@ public class MigrationManager(Configuration config) : IService Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var f = File.Open(path, FileMode.Create, FileAccess.Write); - if (file.IsDawnTrail) + if (file.IsDawntrail) { file.MigrateToDawntrail(); Penumbra.Log.Debug($"Migrated material {reader.Entry.Key} to Dawntrail during import."); @@ -329,7 +329,7 @@ public class MigrationManager(Configuration config) : IService try { var mtrl = new MtrlFile(data); - if (mtrl.IsDawnTrail) + if (mtrl.IsDawntrail) return data; mtrl.MigrateToDawntrail(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c47414b9..e2776b2f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -6,7 +6,6 @@ using OtterGui; using Penumbra.Interop.ResourceTree; using Penumbra.UI.Classes; using Penumbra.String; -using Penumbra.UI.Tabs; namespace Penumbra.UI.AdvancedWindow; @@ -245,10 +244,7 @@ public class ResourceTreeViewer if (visibility == NodeVisibility.Hidden) continue; - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorInternal = (textColor & 0x00FFFFFFu) | ((textColor & 0xFE000000u) >> 1); // Half opacity - - using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); + using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfTransparentText(), resourceNode.Internal); var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; From e8182f285e2fd83ee8c6ed532fa14c858f9557c1 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:22 +0200 Subject: [PATCH 310/865] Update StainService for DT --- .../Interop/Structs/CharacterUtilityData.cs | 28 ++++++- Penumbra/Services/StainService.cs | 84 ++++++++++++++----- Penumbra/UI/Tabs/Debug/DebugTab.cs | 51 +++++++---- 3 files changed, 124 insertions(+), 39 deletions(-) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 197de0bb..7595353f 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,10 +5,15 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexSkinShpk = 83; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -97,8 +102,23 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexDecalTex * 8)] public TextureResourceHandle* DecalTexResource; + [FieldOffset(8 + IndexTileOrbArrayTex * 8)] + public TextureResourceHandle* TileOrbArrayTexResource; + + [FieldOffset(8 + IndexTileNormArrayTex * 8)] + public TextureResourceHandle* TileNormArrayTexResource; + [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] + public ResourceHandle* GudStmResource; + + [FieldOffset(8 + IndexLegacyStm * 8)] + public ResourceHandle* LegacyStmResource; + + [FieldOffset(8 + IndexSphereDArrayTex * 8)] + public TextureResourceHandle* SphereDArrayTexResource; + // not included resources have no known use case. } diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 26b39229..50713968 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -6,19 +6,25 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; -using Penumbra.UI.AdvancedWindow; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.Services; public class StainService : IService { - public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { + // FIXME There might be a better way to handle that. + public int CurrentDyeChannel = 0; + protected override float GetFilterWidth() { var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X; - if (stainCombo.CurrentSelection.Key == 0) + if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) return baseSize; return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; @@ -47,33 +53,73 @@ public class StainService : IService protected override bool DrawSelectable(int globalIdx, bool selected) { var ret = base.DrawSelectable(globalIdx, selected); - var selection = stainCombo.CurrentSelection.Key; + var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) return ret; ImGui.SameLine(); var frame = new Vector2(ImGui.GetTextLineHeight()); - ImGui.ColorButton("D", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Diffuse), 1), 0, frame); + ImGui.ColorButton("D", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("S", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Specular), 1), 0, frame); + ImGui.ColorButton("S", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); ImGui.SameLine(); - ImGui.ColorButton("E", new Vector4(ModEditWindow.PseudoSqrtRgb(colors.Emissive), 1), 0, frame); + ImGui.ColorButton("E", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); return ret; } } - public readonly DictStain StainData; - public readonly FilterComboColors StainCombo; - public readonly StmFile StmFile; - public readonly StainTemplateCombo TemplateCombo; + public const int ChannelCount = 2; - public StainService(IDataManager dataManager, DictStain stainData) + public readonly DictStain StainData; + public readonly FilterComboColors StainCombo1; + public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this? + public readonly StmFile LegacyStmFile; + public readonly StmFile GudStmFile; + public readonly StainTemplateCombo LegacyTemplateCombo; + public readonly StainTemplateCombo GudTemplateCombo; + + public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo = new FilterComboColors(140, MouseWheelType.None, - () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), - Penumbra.Log); - StmFile = new StmFile(dataManager); - TemplateCombo = new StainTemplateCombo(StainCombo, StmFile); + StainData = stainData; + StainCombo1 = CreateStainCombo(); + StainCombo2 = CreateStainCombo(); + LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); + GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + + FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; + + LegacyTemplateCombo = new StainTemplateCombo(stainCombos, LegacyStmFile); + GudTemplateCombo = new StainTemplateCombo(stainCombos, GudStmFile); } + + /// Retrieves the instance for the given channel. Indexing is zero-based. + public FilterComboColors GetStainCombo(int channel) + => channel switch + { + 0 => StainCombo1, + 1 => StainCombo2, + _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)") + }; + + /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + { + if (stmResourceHandle != null) + { + var stmData = stmResourceHandle->CsHandle.GetDataSpan(); + if (stmData.Length > 0) + { + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from ResourceHandle 0x{(nint)stmResourceHandle:X}"); + return new StmFile(stmData); + } + } + + Penumbra.Log.Debug($"[StainService] Loading StmFile<{typeof(TDyePack)}> from Lumina"); + return new StmFile(dataManager); + } + + private FilterComboColors CreateStainCombo() + => new(140, MouseWheelType.None, + () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), + Penumbra.Log); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 3a64e556..ead02874 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,9 @@ using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -697,32 +700,48 @@ public class DebugTab : Window, ITab, IUiService if (!mainTree) return; - foreach (var (key, data) in _stains.StmFile.Entries) + using (var legacyTree = TreeNode("stainingtemplate.stm")) + { + if (legacyTree) + DrawStainTemplatesFile(_stains.LegacyStmFile); + } + + using (var gudTree = TreeNode("stainingtemplate_gud.stm")) + { + if (gudTree) + DrawStainTemplatesFile(_stains.GudStmFile); + } + } + + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + { + foreach (var (key, data) in stmFile.Entries) { using var tree = TreeNode($"Template {key}"); if (!tree) continue; - using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; - for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) + for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) { - var (r, g, b) = data.DiffuseEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + foreach (var list in data.Colors) + { + var color = list[i]; + ImGui.TableNextColumn(); + var frame = new Vector2(ImGui.GetTextLineHeight()); + ImGui.ColorButton("###color", new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)color), 1), 0, frame); + ImGui.SameLine(); + ImGui.TextUnformatted($"{color.Red:F6} | {color.Green:F6} | {color.Blue:F6}"); + } - (r, g, b) = data.SpecularEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - (r, g, b) = data.EmissiveEntries[i]; - ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); - - var a = data.SpecularPowerEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); - - a = data.GlossEntries[i]; - ImGuiUtil.DrawTableColumn($"{a:F6}"); + foreach (var list in data.Scalars) + { + var scalar = list[i]; + ImGuiUtil.DrawTableColumn($"{scalar:F6}"); + } } } } From 450751e43fe4d9627da938715b3930e9d25ebe10 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:47:36 +0200 Subject: [PATCH 311/865] DT material editor, supporting components --- .../Materials/ConstantEditors.cs | 71 +++++++ .../Materials/MaterialTemplatePickers.cs | 177 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs new file mode 100644 index 00000000..690580df --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/ConstantEditors.cs @@ -0,0 +1,71 @@ +using System.Collections.Frozen; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public static class ConstantEditors +{ + public static readonly IEditor DefaultFloat = Editors.DefaultFloat.AsByteEditor(); + public static readonly IEditor DefaultInt = Editors.DefaultInt.AsByteEditor(); + public static readonly IEditor DefaultIntAsFloat = Editors.DefaultInt.IntAsFloatEditor().AsByteEditor(); + public static readonly IEditor DefaultColor = ColorEditor.HighDynamicRange.Reinterpreting(); + + /// + /// Material constants known to be encoded as native s. + /// + /// A editor is nonfunctional for them, as typical values for these constants would fall into the IEEE 754 denormalized number range. + /// + private static readonly FrozenSet KnownIntConstants; + + static ConstantEditors() + { + IReadOnlyList knownIntConstants = [ + "g_ToonIndex", + "g_ToonSpecIndex", + ]; + + KnownIntConstants = knownIntConstants.ToFrozenSet(); + } + + public static IEditor DefaultFor(Name name, MaterialTemplatePickers? materialTemplatePickers = null) + { + if (materialTemplatePickers != null) + { + if (name == Names.SphereMapIndexConstantName) + return materialTemplatePickers.SphereMapIndexPicker; + else if (name == Names.TileIndexConstantName) + return materialTemplatePickers.TileIndexPicker; + } + + if (name.Value != null && name.Value.EndsWith("Color")) + return DefaultColor; + + if (KnownIntConstants.Contains(name)) + return DefaultInt; + + return DefaultFloat; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor AsByteEditor(this IEditor inner) where T : unmanaged + => inner.Reinterpreting(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor IntAsFloatEditor(this IEditor inner) + => inner.Converting(value => int.CreateSaturating(MathF.Round(value)), value => value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithExponent(this IEditor inner, T exponent) + where T : unmanaged, IPowerFunctions, IComparisonOperators + => exponent == T.MultiplicativeIdentity + ? inner + : inner.Converting(value => value < T.Zero ? -T.Pow(-value, T.MultiplicativeIdentity / exponent) : T.Pow(value, T.MultiplicativeIdentity / exponent), value => value < T.Zero ? -T.Pow(-value, exponent) : T.Pow(value, exponent)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEditor WithFactorAndBias(this IEditor inner, T factor, T bias) + where T : unmanaged, IMultiplicativeIdentity, IAdditiveIdentity, IMultiplyOperators, IAdditionOperators, ISubtractionOperators, IDivisionOperators, IEqualityOperators + => factor == T.MultiplicativeIdentity && bias == T.AdditiveIdentity + ? inner + : inner.Converting(value => (value - bias) / factor, value => value * factor + bias); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs new file mode 100644 index 00000000..6ffd1f88 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -0,0 +1,177 @@ +using Dalamud.Interface; +using FFXIVClientStructs.Interop; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed unsafe class MaterialTemplatePickers : IUiService +{ + private const float MaximumTextureSize = 64.0f; + + private readonly TextureArraySlicer _textureArraySlicer; + private readonly CharacterUtility _characterUtility; + + public readonly IEditor TileIndexPicker; + public readonly IEditor SphereMapIndexPicker; + + public MaterialTemplatePickers(TextureArraySlicer textureArraySlicer, CharacterUtility characterUtility) + { + _textureArraySlicer = textureArraySlicer; + _characterUtility = characterUtility; + + TileIndexPicker = new Editor(DrawTileIndexPicker).AsByteEditor(); + SphereMapIndexPicker = new Editor(DrawSphereMapIndexPicker).AsByteEditor(); + } + + public bool DrawTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->TileOrbArrayTexResource, + _characterUtility.Address->TileNormArrayTexResource, + ]); + + public bool DrawSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact) + => _characterUtility.Address != null + && DrawTextureArrayIndexPicker(label, description, ref value, compact, [ + _characterUtility.Address->SphereDArrayTexResource, + ]); + + public bool DrawTextureArrayIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact, ReadOnlySpan> textureRHs) + { + TextureResourceHandle* firstNonNullTextureRH = null; + foreach (var texture in textureRHs) + { + if (texture.Value != null && texture.Value->CsHandle.Texture != null) + { + firstNonNullTextureRH = texture; + break; + } + } + var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null; + + var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; + var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0; + + var ret = false; + + var framePadding = ImGui.GetStyle().FramePadding; + var itemSpacing = ImGui.GetStyle().ItemSpacing; + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + { + var spaceSize = ImUtf8.CalcTextSize(" "u8).X; + var spaces = (int)((ImGui.CalcItemWidth() - framePadding.X * 2.0f - (compact ? 0.0f : (textureSize.X + itemSpacing.X) * textureRHs.Length)) / spaceSize); + using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, framePadding + new Vector2(0.0f, Math.Max(textureSize.Y - ImGui.GetFrameHeight() + itemSpacing.Y, 0.0f) * 0.5f), !compact); + using var combo = ImUtf8.Combo(label, (value == ushort.MaxValue ? "-" : value.ToString()).PadLeft(spaces), ImGuiComboFlags.NoArrowButton | ImGuiComboFlags.HeightLarge); + if (combo.Success && firstNonNullTextureRH != null) + { + var lineHeight = Math.Max(ImGui.GetTextLineHeightWithSpacing(), framePadding.Y * 2.0f + textureSize.Y); + var itemWidth = Math.Max(ImGui.GetContentRegionAvail().X, ImUtf8.CalcTextSize("MMM"u8).X + (itemSpacing.X + textureSize.X) * textureRHs.Length + framePadding.X * 2.0f); + using var center = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); + using var clipper = ImUtf8.ListClipper(count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd && i < count; i++) + { + if (ImUtf8.Selectable($"{i,3}", i == value, size: new(itemWidth, lineHeight))) + { + ret = value != i; + value = (ushort)i; + } + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + var textureRegionStart = new Vector2( + rectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), + rectMin.Y + framePadding.Y); + var maxSize = new Vector2(textureSize.X, rectMax.Y - framePadding.Y - textureRegionStart.Y); + DrawTextureSlices(textureRegionStart, maxSize, itemSpacing.X, textureRHs, (byte)i); + } + } + } + } + if (!compact && value != ushort.MaxValue) + { + var cbRectMin = ImGui.GetItemRectMin(); + var cbRectMax = ImGui.GetItemRectMax(); + var cbTextureRegionStart = new Vector2(cbRectMax.X - framePadding.X - textureSize.X * textureRHs.Length - itemSpacing.X * (textureRHs.Length - 1), cbRectMin.Y + framePadding.Y); + var cbMaxSize = new Vector2(textureSize.X, cbRectMax.Y - framePadding.Y - cbTextureRegionStart.Y); + DrawTextureSlices(cbTextureRegionStart, cbMaxSize, itemSpacing.X, textureRHs, (byte)value); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled) && (description.Length > 0 || compact && value != ushort.MaxValue)) + { + using var disabled = ImRaii.Enabled(); + using var tt = ImUtf8.Tooltip(); + if (description.Length > 0) + ImUtf8.Text(description); + if (compact && value != ushort.MaxValue) + { + ImGui.Dummy(new Vector2(textureSize.X * textureRHs.Length + itemSpacing.X * (textureRHs.Length - 1), textureSize.Y)); + var rectMin = ImGui.GetItemRectMin(); + var rectMax = ImGui.GetItemRectMax(); + DrawTextureSlices(rectMin, textureSize, itemSpacing.X, textureRHs, (byte)value); + } + } + + return ret; + } + + public void DrawTextureSlices(Vector2 regionStart, Vector2 itemSize, float itemSpacing, ReadOnlySpan> textureRHs, byte sliceIndex) + { + for (var j = 0; j < textureRHs.Length; ++j) + { + if (textureRHs[j].Value == null) + continue; + var texture = textureRHs[j].Value->CsHandle.Texture; + if (texture == null) + continue; + var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex); + if (handle == 0) + continue; + + var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; + var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + position += (itemSize - size) * 0.5f; + ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, + new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + } + } + + private delegate bool DrawEditor(ReadOnlySpan label, ReadOnlySpan description, ref ushort value, bool compact); + + private sealed class Editor(DrawEditor draw) : IEditor + { + public bool Draw(Span values, bool disabled) + { + var helper = Editors.PrepareMultiComponent(values.Length); + var ret = false; + + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) + { + helper.SetupComponent(valueIdx); + + var value = ushort.CreateSaturating(MathF.Round(values[valueIdx])); + if (disabled) + { + using var _ = ImRaii.Disabled(); + draw(helper.Id, default, ref value, true); + } + else + { + if (draw(helper.Id, default, ref value, true)) + { + values[valueIdx] = value; + ret = true; + } + } + } + + return ret; + } + } +} From 36ab9573aec81e56655be92f62c7e3e473d737f5 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:41:38 +0200 Subject: [PATCH 312/865] DT material editor, main part --- Penumbra/Configuration.cs | 1 + .../LiveColorTablePreviewer.cs | 24 +- .../MaterialPreview/LiveMaterialPreviewer.cs | 20 +- .../Materials/MtrlTab.CommonColorTable.cs | 509 ++++++++++++ .../Materials/MtrlTab.Constants.cs | 277 +++++++ .../Materials/MtrlTab.Devkit.cs | 240 ++++++ .../Materials/MtrlTab.LegacyColorTable.cs | 368 ++++++++ .../Materials/MtrlTab.LivePreview.cs | 272 ++++++ .../Materials/MtrlTab.ShaderPackage.cs | 505 +++++++++++ .../Materials/MtrlTab.Textures.cs | 276 ++++++ .../UI/AdvancedWindow/Materials/MtrlTab.cs | 199 +++++ .../Materials/MtrlTabFactory.cs | 18 + .../ModEditWindow.Materials.ColorTable.cs | 538 ------------ .../ModEditWindow.Materials.ConstantEditor.cs | 247 ------ .../ModEditWindow.Materials.MtrlTab.cs | 783 ------------------ .../ModEditWindow.Materials.Shpk.cs | 481 ----------- .../AdvancedWindow/ModEditWindow.Materials.cs | 179 +--- .../ModEditWindow.QuickImport.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 76 +- Penumbra/UI/Classes/Colors.cs | 4 +- Penumbra/UI/Tabs/SettingsTab.cs | 12 + 21 files changed, 2744 insertions(+), 2286 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 63325433..50426b38 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -107,6 +107,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool AlwaysOpenDefaultImport { get; set; } = false; public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; + public bool EditRawTileTransforms { get; set; } = false; public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 8e75a895..bbd3b16c 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.GameData.Interop; using Penumbra.Interop.SafeHandles; @@ -7,10 +8,6 @@ namespace Penumbra.Interop.MaterialPreview; public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase { - public const int TextureWidth = 4; - public const int TextureHeight = GameData.Files.MaterialStructs.LegacyColorTable.NumUsedRows; - public const int TextureLength = TextureWidth * TextureHeight * 4; - private readonly IFramework _framework; private readonly Texture** _colorTableTexture; @@ -18,6 +15,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase private bool _updatePending; + public int Width { get; } + public int Height { get; } + public Half[] ColorTable { get; } public LiveColorTablePreviewer(ObjectManager objects, IFramework framework, MaterialInfo materialInfo) @@ -33,18 +33,24 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) throw new InvalidOperationException("Draw object doesn't have color table textures"); - _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + _colorTableTexture = colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); + _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); if (_originalColorTableTexture == null) throw new InvalidOperationException("Material doesn't have a color table"); - ColorTable = new Half[TextureLength]; + Width = (int)_originalColorTableTexture.Texture->Width; + Height = (int)_originalColorTableTexture.Texture->Height; + ColorTable = new Half[Width * Height * 4]; _updatePending = true; framework.Update += OnFrameworkUpdate; } + public Span GetColorRow(int i) + => ColorTable.AsSpan().Slice(Width * 4 * i, Width * 4); + protected override void Clear(bool disposing, bool reset) { _framework.Update -= OnFrameworkUpdate; @@ -74,8 +80,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase return; var textureSize = stackalloc int[2]; - textureSize[0] = TextureWidth; - textureSize[1] = TextureHeight; + textureSize[0] = Width; + textureSize[1] = Height; using var texture = new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false); @@ -104,6 +110,6 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (colorSetTextures == null) return false; - return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * 4 + MaterialInfo.MaterialSlot); + return _colorTableTexture == colorSetTextures + (MaterialInfo.ModelSlot * CharacterBase.MaterialsPerSlot + MaterialInfo.MaterialSlot); } } diff --git a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs index 0556fdc4..60762ac7 100644 --- a/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveMaterialPreviewer.cs @@ -7,9 +7,9 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase { private readonly ShaderPackage* _shaderPackage; - private readonly uint _originalShPkFlags; - private readonly float[] _originalMaterialParameter; - private readonly uint[] _originalSamplerFlags; + private readonly uint _originalShPkFlags; + private readonly byte[] _originalMaterialParameter; + private readonly uint[] _originalSamplerFlags; public LiveMaterialPreviewer(ObjectManager objects, MaterialInfo materialInfo) : base(objects, materialInfo) @@ -28,7 +28,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase _originalShPkFlags = Material->ShaderFlags; - _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); + _originalMaterialParameter = Material->MaterialParameterCBuffer->TryGetBuffer().ToArray(); _originalSamplerFlags = new uint[Material->TextureCount]; for (var i = 0; i < _originalSamplerFlags.Length; ++i) @@ -43,7 +43,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase return; Material->ShaderFlags = _originalShPkFlags; - var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); + var materialParameter = Material->MaterialParameterCBuffer->TryGetBuffer(); if (!materialParameter.IsEmpty) _originalMaterialParameter.AsSpan().CopyTo(materialParameter); @@ -59,7 +59,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase Material->ShaderFlags = shPkFlags; } - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + public void SetMaterialParameter(uint parameterCrc, Index offset, ReadOnlySpan value) { if (!CheckValidity()) return; @@ -68,7 +68,7 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (constantBuffer == null) return; - var buffer = constantBuffer->TryGetBuffer(); + var buffer = constantBuffer->TryGetBuffer(); if (buffer.IsEmpty) return; @@ -78,12 +78,10 @@ public sealed unsafe class LiveMaterialPreviewer : LiveMaterialPreviewerBase if (parameter.CRC != parameterCrc) continue; - if ((parameter.Offset & 0x3) != 0 - || (parameter.Size & 0x3) != 0 - || (parameter.Offset + parameter.Size) >> 2 > buffer.Length) + if (parameter.Offset + parameter.Size > buffer.Length) return; - value.TryCopyTo(buffer.Slice(parameter.Offset >> 2, parameter.Size >> 2)[offset..]); + value.TryCopyTo(buffer.Slice(parameter.Offset, parameter.Size)[offset..]); return; } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs new file mode 100644 index 00000000..937614de --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -0,0 +1,509 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility; +using ImGuiNET; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files; +using OtterGui.Text; +using Penumbra.GameData.Structs; +using OtterGui.Raii; +using OtterGui.Text.Widget; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; + + private static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip); + + private static (Vector2 Scale, float Rotation, float Shear)? _pinnedTileTransform; + + private bool DrawColorTableSection(bool disabled) + { + if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + ColorTableCopyAllClipboardButton(); + ImGui.SameLine(); + var ret = ColorTablePasteAllClipboardButton(disabled); + if (!disabled) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= ColorTableDyeableCheckbox(); + } + + if (Mtrl.DyeTable != null) + { + ImGui.SameLine(); + ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImGui.SameLine(); + ret |= DrawPreviewDye(disabled); + } + + ret |= Mtrl.Table switch + { + LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), + ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + _ => false, + }; + + return ret; + } + + private void ColorTableCopyAllClipboardButton() + { + if (Mtrl.Table == null) + return; + + if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + return; + + try + { + var data1 = Mtrl.Table.AsBytes(); + var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : []; + + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); + } + catch + { + // ignored + } + } + + private bool DrawPreviewDye(bool disabled) + { + var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; + var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; + var tt = dyeId1 == 0 && dyeId2 == 0 + ? "Select a preview dye first."u8 + : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8; + if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0)) + { + var ret = false; + if (Mtrl.DyeTable != null) + { + ret |= Mtrl.ApplyDye(_stainService.LegacyStmFile, [dyeId1, dyeId2]); + ret |= Mtrl.ApplyDye(_stainService.GudStmFile, [dyeId1, dyeId2]); + } + + UpdateColorTablePreview(); + + return ret; + } + + ImGui.SameLine(); + var label = dyeId1 == 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1"; + if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1)) + UpdateColorTablePreview(); + ImGui.SameLine(); + label = dyeId2 == 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2"; + if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2)) + UpdateColorTablePreview(); + return false; + } + + private bool ColorTablePasteAllClipboardButton(bool disabled) + { + if (Mtrl.Table == null) + return false; + + if (!ImUtf8.ButtonEx("Import All Rows from Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0), disabled)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var table = Mtrl.Table.AsBytes(); + var dyeTable = Mtrl.DyeTable != null ? Mtrl.DyeTable.AsBytes() : []; + if (data.Length != table.Length && data.Length != table.Length + dyeTable.Length) + return false; + + data.AsSpan(0, table.Length).TryCopyTo(table); + data.AsSpan(table.Length).TryCopyTo(dyeTable); + + UpdateColorTablePreview(); + + return true; + } + catch + { + return false; + } + } + + [SkipLocalsInit] + private void ColorTableCopyClipboardButton(int rowIdx) + { + if (Mtrl.Table == null) + return; + + if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, "Export this row to your clipboard."u8, + ImGui.GetFrameHeight() * Vector2.One)) + return; + + try + { + var data1 = Mtrl.Table.RowAsBytes(rowIdx); + var data2 = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + + var array = new byte[data1.Length + data2.Length]; + data1.TryCopyTo(array); + data2.TryCopyTo(array.AsSpan(data1.Length)); + + var text = Convert.ToBase64String(array); + ImGui.SetClipboardText(text); + } + catch + { + // ignored + } + } + + private bool ColorTableDyeableCheckbox() + { + var dyeable = Mtrl.DyeTable != null; + var ret = ImGui.Checkbox("Dyeable", ref dyeable); + + if (ret) + { + Mtrl.DyeTable = dyeable ? Mtrl.Table switch + { + ColorTable => new ColorDyeTable(), + LegacyColorTable => new LegacyColorDyeTable(), + _ => null, + } : null; + UpdateColorTablePreview(); + } + + return ret; + } + + private bool ColorTablePasteFromClipboardButton(int rowIdx, bool disabled) + { + if (Mtrl.Table == null) + return false; + + if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled)) + return false; + + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var row = Mtrl.Table.RowAsBytes(rowIdx); + var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + return false; + + data.AsSpan(0, row.Length).TryCopyTo(row); + data.AsSpan(row.Length).TryCopyTo(dyeRow); + + UpdateColorTableRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + } + + private void ColorTableHighlightButton(int pairIdx, bool disabled) + { + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0); + + if (ImGui.IsItemHovered()) + HighlightColorTablePair(pairIdx); + else if (HighlightedColorTablePair == pairIdx) + CancelColorTableHighlight(); + } + + private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) + { + var style = ImGui.GetStyle(); + var frameRounding = style.FrameRounding; + var frameThickness = style.FrameBorderSize; + var borderColor = ImGui.GetColorU32(ImGuiCol.Border); + var drawList = ImGui.GetWindowDrawList(); + if (topColor == bottomColor) + drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + else + { + drawList.AddRectFilled( + rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, + topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight); + drawList.AddRectFilledMultiColor( + rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, + rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, + topColor, topColor, bottomColor, bottomColor); + drawList.AddRectFilled( + rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, + bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); + } + drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); + } + + private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, ReadOnlySpan letter = default) + { + var ret = false; + var inputSqrt = PseudoSqrtRgb((Vector3)current); + var tmp = inputSqrt; + if (ImUtf8.ColorEdit(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR) + && tmp != inputSqrt) + { + setter((HalfColor)PseudoSquareRgb(tmp)); + ret = true; + } + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textColor = inputSqrt.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText(letter, center, textColor); + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + + return ret; + } + + private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, ReadOnlySpan letter = default) + { + if (current.HasValue) + CtColorPicker(label, description, current.Value, Nop, letter); + else + { + var tmp = Vector4.Zero; + ImUtf8.ColorEdit(label, ref tmp, + ImGuiColorEditFlags.NoInputs + | ImGuiColorEditFlags.DisplayRGB + | ImGuiColorEditFlags.InputRGB + | ImGuiColorEditFlags.NoTooltip + | ImGuiColorEditFlags.HDR + | ImGuiColorEditFlags.AlphaPreview); + + if (letter.Length > 0 && ImGui.IsItemVisible()) + { + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u); + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + } + } + + private static bool CtApplyStainCheckbox(ReadOnlySpan label, ReadOnlySpan description, bool current, Action setter) + { + var tmp = current; + var result = ApplyStainCheckbox.Draw(label, ref tmp); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == current) + return false; + + setter(tmp); + return true; + } + + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, float max, float speed, Action setter) + { + var tmp = (float)value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result) + return false; + var newValue = (Half)tmp; + if (newValue == value) + return false; + setter(newValue); + return true; + } + + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, float min, float max, float speed) + { + var tmp = (float)value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result) + return false; + var newValue = (Half)tmp; + if (newValue == value) + return false; + value = newValue; + return true; + } + + private static void CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half? value, ReadOnlySpan format) + { + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? Half.Zero; + var floatValue = (float)valueOrDefault; + CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop); + } + + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, T max, float speed, Action setter) where T : unmanaged, INumber + { + var tmp = value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == value) + return false; + setter(tmp); + return true; + } + + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, T max, float speed) where T : unmanaged, INumber + { + var tmp = value; + var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); + if (!result || tmp == value) + return false; + value = tmp; + return true; + } + + private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) where T : unmanaged, INumber + { + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? T.Zero; + CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop); + } + + private bool CtTileIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + { + if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact)) + return false; + setter(value); + return true; + } + + private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + { + if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact)) + return false; + setter(value); + return true; + } + + private bool CtTileTransformMatrix(HalfMatrix2x2 value, float floatSize, bool twoRowLayout, Action setter) + { + var ret = false; + if (_config.EditRawTileTransforms) + { + var tmp = value; + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformUU"u8, "Tile Repeat U"u8, ref tmp.UU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformVV"u8, "Tile Repeat V"u8, ref tmp.VV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + if (!twoRowLayout) + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformUV"u8, "Tile Skew U"u8, ref tmp.UV, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + if (!ret || tmp == value) + return false; + setter(tmp); + } + else + { + value.Decompose(out var scale, out var rotation, out var shear); + rotation *= 180.0f / MathF.PI; + shear *= 180.0f / MathF.PI; + ImGui.SetNextItemWidth(floatSize); + var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + var activated = ImGui.IsItemActivated(); + var deactivated = ImGui.IsItemDeactivated(); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + if (!twoRowLayout) + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); + if (deactivated) + _pinnedTileTransform = null; + else if (activated) + _pinnedTileTransform = (scale, rotation, shear); + ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged; + if (!ret) + return false; + if (_pinnedTileTransform.HasValue) + { + var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value; + if (!scaleXChanged) + scale.X = pinScale.X; + if (!scaleYChanged) + scale.Y = pinScale.Y; + if (!rotationChanged) + rotation = pinRotation; + if (!shearChanged) + shear = pinShear; + } + var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f); + if (newValue == value) + return false; + setter(newValue); + } + return true; + } + + /// For use as setter of read-only fields. + private static void Nop(T _) + { } + + // Functions to deal with squared RGB values without making negatives useless. + + internal static float PseudoSquareRgb(float x) + => x < 0.0f ? -(x * x) : x * x; + + internal static Vector3 PseudoSquareRgb(Vector3 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); + + internal static Vector4 PseudoSquareRgb(Vector4 vec) + => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); + + internal static float PseudoSqrtRgb(float x) + => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); + + internal static Vector3 PseudoSqrtRgb(Vector3 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); + + internal static Vector4 PseudoSqrtRgb(Vector4 vec) + => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs new file mode 100644 index 00000000..56496005 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -0,0 +1,277 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Text.Widget.Editors; +using Penumbra.GameData.Files.ShaderStructs; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float MaterialConstantSize = 250.0f; + + public readonly + List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IEditor Editor)> + Constants)> Constants = new(16); + + private void UpdateConstants() + { + static List FindOrAddGroup(List<(string, List)> groups, string name) + { + foreach (var (groupName, group) in groups) + { + if (string.Equals(name, groupName, StringComparison.Ordinal)) + return group; + } + + var newGroup = new List(16); + groups.Add((name, newGroup)); + return newGroup; + } + + Constants.Clear(); + string mpPrefix; + if (AssociatedShpk == null) + { + mpPrefix = MaterialParamsConstantName.Value!; + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) + { + var values = Mtrl.GetConstantValue(constant); + for (var i = 0; i < values.Length; i += 4) + { + fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, + ConstantEditors.DefaultFloat)); + } + } + } + else + { + mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; + var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8); + foreach (var shpkConstant in AssociatedShpk.MaterialParams) + { + var name = Names.KnownNames.TryResolve(shpkConstant.Id); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex); + var values = Mtrl.GetConstantValue(constant); + var handledElements = new IndexSet(values.Length, false); + + var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); + if (dkData != null) + foreach (var dkConstant in dkData) + { + var offset = (int)dkConstant.EffectiveByteOffset; + var length = values.Length - offset; + var constantSize = dkConstant.EffectiveByteSize; + if (constantSize.HasValue) + length = Math.Min(length, (int)constantSize.Value); + if (length <= 0) + continue; + + var editor = dkConstant.CreateEditor(_materialTemplatePickers); + if (editor != null) + FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") + .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); + handledElements.AddRange(offset, length); + } + + if (handledElements.IsFull) + continue; + + var fcGroup = FindOrAddGroup(Constants, "Further Constants"); + foreach (var (start, end) in handledElements.Ranges(complement: true)) + { + if (start == 0 && end == values.Length && end - start <= 16) + { + if (name.Value != null) + { + fcGroup.Add(( + $"{name.Value.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", + constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name))); + continue; + } + } + + if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0) + { + var offset = shpkConstant.ByteOffset; + for (int i = (start & ~0xF) - (offset & 0xF), j = offset >> 4; i < end; i += 16, ++j) + { + var rangeStart = Math.Max(i, start); + var rangeEnd = Math.Min(i + 16, end); + if (rangeEnd > rangeStart) + { + var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; + fcGroup.Add(( + $"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", + constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name))); + } + } + } + else + { + for (var i = start; i < end; i += 16) + { + fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true, + DefaultConstantEditorFor(name))); + } + } + } + } + } + + Constants.RemoveAll(group => group.Constants.Count == 0); + Constants.Sort((x, y) => + { + if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) + return 1; + if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) + return -1; + + return string.Compare(x.Header, y.Header, StringComparison.Ordinal); + }); + // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme, and cbuffer-location names appear after known variable names + foreach (var (_, group) in Constants) + { + group.Sort((x, y) => string.CompareOrdinal( + x.MonoFont ? x.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : x.Label, + y.MonoFont ? y.Label.Replace("].w", "].{").Replace(mpPrefix, "}_MaterialParameter") : y.Label)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IEditor DefaultConstantEditorFor(Name name) + => ConstantEditors.DefaultFor(name, _materialTemplatePickers); + + private bool DrawConstantsSection(bool disabled) + { + if (Constants.Count == 0) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Material Constants")) + return false; + + using var _ = ImRaii.PushId("MaterialConstants"); + + var ret = false; + foreach (var (header, group) in Constants) + { + using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); + if (!t) + continue; + + foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) + { + var constant = Mtrl.ShaderPackage.Constants[constantIndex]; + var buffer = Mtrl.GetConstantValue(constant); + if (buffer.Length > 0) + { + using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); + ImGui.SetNextItemWidth(MaterialConstantSize * UiHelpers.Scale); + if (editor.Draw(buffer[slice], disabled)) + { + ret = true; + SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id); + var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; + var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : []; + var canReset = AssociatedShpk?.MaterialParamsDefaults != null + ? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice]) + : buffer[slice].ContainsAnyExcept((byte)0); + ImUtf8.SameLineInner(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) + { + ret = true; + if (defaultValue.Length > 0) + defaultValue.CopyTo(buffer[slice]); + else + buffer[slice].Clear(); + + SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); + } + + ImGui.SameLine(); + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + } + } + + return ret; + } + + private static bool IsValid(Range range, int length) + { + var start = range.Start.GetOffset(length); + var end = range.End.GetOffset(length); + return start >= 0 && start <= length && end >= start && end <= length; + } + + internal static string? MaterialParamName(bool componentOnly, int offset) + { + if (offset < 0) + return null; + + return (componentOnly, offset & 0x3) switch + { + (true, 0) => "x", + (true, 1) => "y", + (true, 2) => "z", + (true, 3) => "w", + (false, 0) => $"[{offset >> 2:D2}].x", + (false, 1) => $"[{offset >> 2:D2}].y", + (false, 2) => $"[{offset >> 2:D2}].z", + (false, 3) => $"[{offset >> 2:D2}].w", + _ => null, + }; + } + + /// Returned string is 4 chars long. + private static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch + { + (0, 4) => " ", + (0, 0) => ".x ", + (0, 1) => ".xy ", + (0, 2) => ".xyz", + (0, 3) => " ", + (1, 1) => ".y ", + (1, 2) => ".yz ", + (1, 3) => ".yzw", + (2, 2) => ".z ", + (2, 3) => ".zw ", + (3, 3) => ".w ", + _ => string.Empty, + }; + + internal static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) + { + if (valueLength == 0 || valueOffset < 0) + return (null, false); + + var firstVector = valueOffset >> 2; + var lastVector = (valueOffset + valueLength - 1) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = (valueOffset + valueLength - 1) & 0x3; + if (firstVector == lastVector) + return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); + + var sb = new StringBuilder(128); + sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); + for (var i = firstVector + 1; i < lastVector; ++i) + sb.Append($", [{i}]"); + + sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); + return (sb.ToString(), false); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs new file mode 100644 index 00000000..cd62d58f --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs @@ -0,0 +1,240 @@ +using Newtonsoft.Json.Linq; +using OtterGui.Text.Widget.Editors; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) + { + try + { + if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) + throw new Exception("Could not assemble ShPk dev-kit path."); + + var devkitFullPath = _edit.FindBestMatch(devkitPath); + if (!devkitFullPath.IsRooted) + throw new Exception("Could not resolve ShPk dev-kit path."); + + devkitPathName = devkitFullPath.FullName; + return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); + } + catch + { + devkitPathName = string.Empty; + return null; + } + } + + private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class + => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + + private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class + { + if (devkit == null) + return null; + + try + { + var data = devkit[category]; + if (id.HasValue) + data = data?[id.Value.ToString()]; + + if (mayVary && (data as JObject)?["Vary"] != null) + { + var selector = BuildSelector(data!["Vary"]! + .Select(key => (uint)key) + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + var index = (int)data["Selectors"]![selector.ToString()]!; + data = data["Items"]![index]; + } + + return data?.ToObject(typeof(T)) as T; + } + catch (Exception e) + { + // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) + Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); + return null; + } + } + + private sealed class DevkitShaderKeyValue + { + public string Label = string.Empty; + public string Description = string.Empty; + } + + private sealed class DevkitShaderKey + { + public string Label = string.Empty; + public string Description = string.Empty; + public Dictionary Values = []; + } + + private sealed class DevkitSampler + { + public string Label = string.Empty; + public string Description = string.Empty; + public string DefaultTexture = string.Empty; + } + + private enum DevkitConstantType + { + Hidden = -1, + Float = 0, + /// Integer encoded as a float. + Integer = 1, + Color = 2, + Enum = 3, + /// Native integer. + Int32 = 4, + Int32Enum = 5, + Int8 = 6, + Int8Enum = 7, + Int16 = 8, + Int16Enum = 9, + Int64 = 10, + Int64Enum = 11, + Half = 12, + Double = 13, + TileIndex = 14, + SphereMapIndex = 15, + } + + private sealed class DevkitConstantValue + { + public string Label = string.Empty; + public string Description = string.Empty; + public double Value = 0; + } + + private sealed class DevkitConstant + { + public uint Offset = 0; + public uint? Length = null; + public uint? ByteOffset = null; + public uint? ByteSize = null; + public string Group = string.Empty; + public string Label = string.Empty; + public string Description = string.Empty; + public DevkitConstantType Type = DevkitConstantType.Float; + + public float? Minimum = null; + public float? Maximum = null; + public float Step = 0.0f; + public float StepFast = 0.0f; + public float? Speed = null; + public float RelativeSpeed = 0.0f; + public float Exponent = 1.0f; + public float Factor = 1.0f; + public float Bias = 0.0f; + public byte Precision = 3; + public bool Hex = false; + public bool Slider = true; + public bool Drag = true; + public string Unit = string.Empty; + + public bool SquaredRgb = false; + public bool Clamped = false; + + public DevkitConstantValue[] Values = []; + + public uint EffectiveByteOffset + => ByteOffset ?? Offset * ValueSize; + + public uint? EffectiveByteSize + => ByteSize ?? (Length * ValueSize); + + public unsafe uint ValueSize + => Type switch + { + DevkitConstantType.Hidden => sizeof(byte), + DevkitConstantType.Float => sizeof(float), + DevkitConstantType.Integer => sizeof(float), + DevkitConstantType.Color => sizeof(float), + DevkitConstantType.Enum => sizeof(float), + DevkitConstantType.Int32 => sizeof(int), + DevkitConstantType.Int32Enum => sizeof(int), + DevkitConstantType.Int8 => sizeof(byte), + DevkitConstantType.Int8Enum => sizeof(byte), + DevkitConstantType.Int16 => sizeof(short), + DevkitConstantType.Int16Enum => sizeof(short), + DevkitConstantType.Int64 => sizeof(long), + DevkitConstantType.Int64Enum => sizeof(long), + DevkitConstantType.Half => (uint)sizeof(Half), + DevkitConstantType.Double => sizeof(double), + DevkitConstantType.TileIndex => sizeof(float), + DevkitConstantType.SphereMapIndex => sizeof(float), + _ => sizeof(float), + }; + + public IEditor? CreateEditor(MaterialTemplatePickers? materialTemplatePickers) + => Type switch + { + DevkitConstantType.Hidden => null, + DevkitConstantType.Float => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.Integer => CreateIntegerEditor().IntAsFloatEditor().AsByteEditor(), + DevkitConstantType.Color => ColorEditor.Get(!Clamped).WithExponent(SquaredRgb ? 2.0f : 1.0f).AsByteEditor(), + DevkitConstantType.Enum => CreateEnumEditor(float.CreateSaturating).AsByteEditor(), + DevkitConstantType.Int32 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int32Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Int8 => CreateIntegerEditor(), + DevkitConstantType.Int8Enum => CreateEnumEditor(ToInteger), + DevkitConstantType.Int16 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int16Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Int64 => CreateIntegerEditor().AsByteEditor(), + DevkitConstantType.Int64Enum => CreateEnumEditor(ToInteger).AsByteEditor(), + DevkitConstantType.Half => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.Double => CreateFloatEditor().AsByteEditor(), + DevkitConstantType.TileIndex => materialTemplatePickers?.TileIndexPicker ?? ConstantEditors.DefaultIntAsFloat, + DevkitConstantType.SphereMapIndex => materialTemplatePickers?.SphereMapIndexPicker ?? ConstantEditors.DefaultIntAsFloat, + _ => ConstantEditors.DefaultFloat, + }; + + private IEditor CreateIntegerEditor() + where T : unmanaged, INumber + => ((Drag || Slider) && !Hex + ? (Drag + ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0) + : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0)) + : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), Hex, Unit, 0)) + .WithFactorAndBias(ToInteger(Factor), ToInteger(Bias)); + + private IEditor CreateFloatEditor() + where T : unmanaged, INumber, IPowerFunctions + => ((Drag || Slider) + ? (Drag + ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0) + : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0)) + : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0)) + .WithExponent(T.CreateSaturating(Exponent)) + .WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias)); + + private EnumEditor CreateEnumEditor(Func convertValue) + where T : unmanaged, IUtf8SpanFormattable, IEqualityOperators + => new(Array.ConvertAll(Values, value => (ToUtf8(value.Label), convertValue(value.Value), ToUtf8(value.Description)))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ToInteger(float value) where T : struct, INumberBase + => T.CreateSaturating(MathF.Round(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ToInteger(double value) where T : struct, INumberBase + => T.CreateSaturating(Math.Round(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? ToInteger(float? value) where T : struct, INumberBase + => value.HasValue ? ToInteger(value.Value) : null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T? ToFloat(float? value) where T : struct, INumberBase + => value.HasValue ? T.CreateSaturating(value.Value) : null; + + private static ReadOnlyMemory ToUtf8(string value) + => Encoding.UTF8.GetBytes(value); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs new file mode 100644 index 00000000..f3ec5307 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -0,0 +1,368 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float LegacyColorTableFloatSize = 65.0f; + private const float LegacyColorTablePercentageSize = 50.0f; + private const float LegacyColorTableIntegerSize = 40.0f; + private const float LegacyColorTableByteSize = 25.0f; + + private bool DrawLegacyColorTable(LegacyColorTable table, LegacyColorDyeTable? dyeTable, bool disabled) + { + using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!imTable) + return false; + + DrawLegacyColorTableHeader(dyeTable != null); + + var ret = false; + for (var i = 0; i < LegacyColorTable.NumRows; ++i) + { + if (DrawLegacyColorTableRow(table, dyeTable, i, disabled)) + { + UpdateColorTableRowPreview(i); + ret = true; + } + ImGui.TableNextRow(); + } + + return ret; + } + + private bool DrawLegacyColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + using var imTable = ImUtf8.Table("##ColorTable"u8, dyeTable != null ? 10 : 8, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); + if (!imTable) + return false; + + DrawLegacyColorTableHeader(dyeTable != null); + + var ret = false; + for (var i = 0; i < ColorTable.NumRows; ++i) + { + if (DrawLegacyColorTableRow(table, dyeTable, i, disabled)) + { + UpdateColorTableRowPreview(i); + ret = true; + } + ImGui.TableNextRow(); + } + + return ret; + } + + private static void DrawLegacyColorTableHeader(bool hasDyeTable) + { + ImGui.TableNextColumn(); + ImUtf8.TableHeader(default(ReadOnlySpan)); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Row"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Diffuse"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Specular"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Emissive"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Gloss"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Tile"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Repeat / Skew"u8); + if (hasDyeTable) + { + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Dye"u8); + ImGui.TableNextColumn(); + ImUtf8.TableHeader("Dye Preview"u8); + } + } + + private bool DrawLegacyColorTableRow(LegacyColorTable table, LegacyColorDyeTable? dyeTable, int rowIdx, bool disabled) + { + using var id = ImRaii.PushId(rowIdx); + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; + var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; + var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + if ((rowIdx & 1) == 0) + { + ImUtf8.SameLineInner(); + ColorTableHighlightButton(rowIdx >> 1, disabled); + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SpecularMask = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.SpecularMask, + b => dyeTable[rowIdx].SpecularMask = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f), + v => table[rowIdx].Shininess = v); + + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Shininess, + b => dyeTable[rowIdx].Shininess = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(intSize); + ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true, + value => table[rowIdx].TileIndex = value); + + ImGui.TableNextColumn(); + ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, + m => table[rowIdx].TileTransform = m); + + if (dyeTable != null) + { + ImGui.TableNextColumn(); + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize); + } + + return ret; + } + + private bool DrawLegacyColorTableRow(ColorTable table, ColorDyeTable? dyeTable, int rowIdx, bool disabled) + { + using var id = ImRaii.PushId(rowIdx); + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; + var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; + var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; + var byteSize = LegacyColorTableByteSize * UiHelpers.Scale; + ImGui.TableNextColumn(); + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + if ((rowIdx & 1) == 0) + { + ImUtf8.SameLineInner(); + ColorTableHighlightButton(rowIdx >> 1, disabled); + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= CtColorPicker("##Diffuse"u8, "Diffuse Color"u8, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeDiffuse"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Specular"u8, "Specular Color"u8, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Scalar7 = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeSpecularMask"u8, "Apply Specular Strength on Dye"u8, dye.Metalness, + b => dyeTable[rowIdx].Metalness = b); + } + + ImGui.TableNextColumn(); + ret |= CtColorPicker("##Emissive"u8, "Emissive Color"u8, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeEmissive"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(floatSize); + var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f), + v => table[rowIdx].Scalar3 = v); + + if (dyeTable != null) + { + ImUtf8.SameLineInner(); + ret |= CtApplyStainCheckbox("##dyeShininess"u8, "Apply Gloss Strength on Dye"u8, dye.Scalar3, + b => dyeTable[rowIdx].Scalar3 = b); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(intSize); + ret |= CtTileIndexPicker("##TileIndex"u8, "Tile Index"u8, row.TileIndex, true, + value => table[rowIdx].TileIndex = value); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(pctSize); + ret |= CtDragScalar("##TileAlpha"u8, "Tile Opacity"u8, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].TileAlpha = (Half)(v * 0.01f)); + + ImGui.TableNextColumn(); + ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, + m => table[rowIdx].TileTransform = m); + + if (dyeTable != null) + { + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(byteSize); + ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f, + value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); + ImUtf8.SameLineInner(); + _stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel; + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + + ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); + + ImGui.TableNextColumn(); + ret |= DrawLegacyDyePreview(rowIdx, disabled, dye, floatSize); + } + + return ret; + } + + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize) + { + var stain = _stainService.StainCombo1.CurrentSelection.Key; + if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + return false; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); + + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); + + ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [stain], rowIdx); + + ImGui.SameLine(); + DrawLegacyDyePreview(values, floatSize); + + return ret; + } + + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + { + var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; + if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + return false; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); + + var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Apply the selected dye to this row.", disabled, true); + + ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); + + ImGui.SameLine(); + DrawLegacyDyePreview(values, floatSize); + + return ret; + } + + private static void DrawLegacyDyePreview(LegacyDyePack values, float floatSize) + { + CtColorPicker("##diffusePreview"u8, default, values.DiffuseColor, "D"u8); + ImUtf8.SameLineInner(); + CtColorPicker("##specularPreview"u8, default, values.SpecularColor, "S"u8); + ImUtf8.SameLineInner(); + CtColorPicker("##emissivePreview"u8, default, values.EmissiveColor, "E"u8); + ImUtf8.SameLineInner(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth(floatSize); + var shininess = (float)values.Shininess; + ImGui.DragFloat("##shininessPreview", ref shininess, 0, shininess, shininess, "%.1f G"); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(floatSize); + var specularMask = (float)values.SpecularMask * 100.0f; + ImGui.DragFloat("##specularMaskPreview", ref specularMask, 0, specularMask, specularMask, "%.0f%% S"); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs new file mode 100644 index 00000000..bb346534 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -0,0 +1,272 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Interop.MaterialPreview; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + public readonly List MaterialPreviewers = new(4); + public readonly List ColorTablePreviewers = new(4); + public int HighlightedColorTablePair = -1; + public readonly Stopwatch HighlightTime = new(); + + private void DrawMaterialLivePreviewRebind(bool disabled) + { + if (disabled) + return; + + if (ImGui.Button("Reload live preview")) + BindToMaterialInstances(); + + if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0) + return; + + ImGui.SameLine(); + using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImUtf8.Text( + "The current material has not been found on your character. Please check the Import from Screen tab for more information."u8); + } + + public unsafe void BindToMaterialInstances() + { + UnbindFromMaterialInstances(); + + var instances = MaterialInfo.FindMaterials(_resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), + FilePath); + + var foundMaterials = new HashSet(); + foreach (var materialInfo in instances) + { + var material = materialInfo.GetDrawObjectMaterial(_objects); + if (foundMaterials.Contains((nint)material)) + continue; + + try + { + MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); + foundMaterials.Add((nint)material); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateMaterialPreview(); + + if (Mtrl.Table == null) + return; + + foreach (var materialInfo in instances) + { + try + { + ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); + } + catch (InvalidOperationException) + { + // Carry on without that previewer. + } + } + + UpdateColorTablePreview(); + } + + private void UnbindFromMaterialInstances() + { + foreach (var previewer in MaterialPreviewers) + previewer.Dispose(); + MaterialPreviewers.Clear(); + + foreach (var previewer in ColorTablePreviewers) + previewer.Dispose(); + ColorTablePreviewers.Clear(); + } + + private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) + { + for (var i = MaterialPreviewers.Count; i-- > 0;) + { + var previewer = MaterialPreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + MaterialPreviewers.RemoveAt(i); + } + + for (var i = ColorTablePreviewers.Count; i-- > 0;) + { + var previewer = ColorTablePreviewers[i]; + if (previewer.DrawObject != characterBase) + continue; + + previewer.Dispose(); + ColorTablePreviewers.RemoveAt(i); + } + } + + public void SetShaderPackageFlags(uint shPkFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetShaderPackageFlags(shPkFlags); + } + + public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetMaterialParameter(parameterCrc, offset, value); + } + + public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + { + foreach (var previewer in MaterialPreviewers) + previewer.SetSamplerFlags(samplerCrc, samplerFlags); + } + + private void UpdateMaterialPreview() + { + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + foreach (var constant in Mtrl.ShaderPackage.Constants) + { + var values = Mtrl.GetConstantValue(constant); + if (values != null) + SetMaterialParameter(constant.Id, 0, values); + } + + foreach (var sampler in Mtrl.ShaderPackage.Samplers) + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + public void HighlightColorTablePair(int pairIdx) + { + var oldPairIdx = HighlightedColorTablePair; + + if (HighlightedColorTablePair != pairIdx) + { + HighlightedColorTablePair = pairIdx; + HighlightTime.Restart(); + } + + if (oldPairIdx >= 0) + { + UpdateColorTableRowPreview(oldPairIdx << 1); + UpdateColorTableRowPreview((oldPairIdx << 1) | 1); + } + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + public void CancelColorTableHighlight() + { + var pairIdx = HighlightedColorTablePair; + + HighlightedColorTablePair = -1; + HighlightTime.Reset(); + + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + public void UpdateColorTableRowPreview(int rowIdx) + { + if (ColorTablePreviewers.Count == 0) + return; + + if (Mtrl.Table == null) + return; + + var row = Mtrl.Table switch + { + LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]), + ColorTable table => table[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), + }; + if (Mtrl.DyeTable != null) + { + var dyeRow = Mtrl.DyeTable switch + { + LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]), + ColorDyeTable dyeTable => dyeTable[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), + }; + if (dyeRow.Channel < StainService.ChannelCount) + { + StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key; + if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes)) + row.ApplyDye(dyeRow, legacyDyes); + if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes)) + row.ApplyDye(dyeRow, gudDyes); + } + } + + if (HighlightedColorTablePair << 1 == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); + else if (((HighlightedColorTablePair << 1) | 1) == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + + foreach (var previewer in ColorTablePreviewers) + { + row[..].CopyTo(previewer.GetColorRow(rowIdx)); + previewer.ScheduleUpdate(); + } + } + + public void UpdateColorTablePreview() + { + if (ColorTablePreviewers.Count == 0) + return; + + if (Mtrl.Table == null) + return; + + var rows = new ColorTable(Mtrl.Table); + var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null; + if (dyeRows != null) + { + ReadOnlySpan stainIds = [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ]; + rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows); + rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); + } + + if (HighlightedColorTablePair >= 0) + { + ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + } + + foreach (var previewer in ColorTablePreviewers) + { + rows.AsHalves().CopyTo(previewer.ColorTable); + previewer.ScheduleUpdate(); + } + } + + private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time) + { + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = colorId.Value(); + var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); + var halfColor = (HalfColor)(color * color); + + row.DiffuseColor = halfColor; + row.SpecularColor = halfColor; + row.EmissiveColor = halfColor; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs new file mode 100644 index 00000000..21557939 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -0,0 +1,505 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' + // Apricot shader packages are unlisted because + // 1. they cause severe performance/memory issues when calculating the effective shader set + // 2. they probably aren't intended for use with materials anyway + internal static readonly IReadOnlyList StandardShaderPackages = new[] + { + "3dui.shpk", + // "apricot_decal_dummy.shpk", + // "apricot_decal_ring.shpk", + // "apricot_decal.shpk", + // "apricot_fogModel.shpk", + // "apricot_gbuffer_decal_dummy.shpk", + // "apricot_gbuffer_decal_ring.shpk", + // "apricot_gbuffer_decal.shpk", + // "apricot_lightmodel.shpk", + // "apricot_model_dummy.shpk", + // "apricot_model_morph.shpk", + // "apricot_model.shpk", + // "apricot_powder_dummy.shpk", + // "apricot_powder.shpk", + // "apricot_shape_dummy.shpk", + // "apricot_shape.shpk", + "bgcolorchange.shpk", + "bg_composite.shpk", + "bgcrestchange.shpk", + "bgdecal.shpk", + "bgprop.shpk", + "bg.shpk", + "bguvscroll.shpk", + "characterglass.shpk", + "characterinc.shpk", + "characterlegacy.shpk", + "characterocclusion.shpk", + "characterreflection.shpk", + "characterscroll.shpk", + "charactershadowoffset.shpk", + "character.shpk", + "characterstockings.shpk", + "charactertattoo.shpk", + "charactertransparency.shpk", + "cloud.shpk", + "createviewposition.shpk", + "crystal.shpk", + "directionallighting.shpk", + "directionalshadow.shpk", + "furblur.shpk", + "grassdynamicwave.shpk", + "grass.shpk", + "hairmask.shpk", + "hair.shpk", + "iris.shpk", + "lightshaft.shpk", + "linelighting.shpk", + "planelighting.shpk", + "pointlighting.shpk", + "river.shpk", + "shadowmask.shpk", + "skin.shpk", + "spotlighting.shpk", + "subsurfaceblur.shpk", + "verticalfog.shpk", + "water.shpk", + "weather.shpk", + }; + + private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???"); + + private string[]? _shpkNames; + + public string ShaderHeader = "Shader###Shader"; + public FullPath LoadedShpkPath = FullPath.Empty; + public string LoadedShpkPathName = string.Empty; + public string LoadedShpkDevkitPathName = string.Empty; + public string ShaderComment = string.Empty; + public ShpkFile? AssociatedShpk; + public bool ShpkLoading; + public JObject? AssociatedShpkDevkit; + + public readonly string LoadedBaseDevkitPathName; + public readonly JObject? AssociatedBaseDevkit; + + // Shader Key State + public readonly + List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> + Values)> ShaderKeys = new(16); + + public readonly HashSet VertexShaders = new(16); + public readonly HashSet PixelShaders = new(16); + public bool ShadersKnown; + public ReadOnlyMemory ShadersString = UnknownShadersString; + + public string[] GetShpkNames() + { + if (null != _shpkNames) + return _shpkNames; + + var names = new HashSet(StandardShaderPackages); + names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); + + _shpkNames = names.ToArray(); + Array.Sort(_shpkNames); + + return _shpkNames; + } + + public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + { + defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) + return FullPath.Empty; + + return _edit.FindBestMatch(defaultGamePath); + } + + public void LoadShpk(FullPath path) + => Task.Run(() => DoLoadShpk(path)); + + private async Task DoLoadShpk(FullPath path) + { + ShadersKnown = false; + ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + ShpkLoading = true; + + try + { + var data = path.IsRooted + ? await File.ReadAllBytesAsync(path.FullName) + : _gameData.GetFile(path.InternalName.ToString())?.Data; + LoadedShpkPath = path; + AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + LoadedShpkPathName = path.ToPath(); + } + catch (Exception e) + { + LoadedShpkPath = FullPath.Empty; + LoadedShpkPathName = string.Empty; + AssociatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + } + finally + { + ShpkLoading = false; + } + + if (LoadedShpkPath.InternalName.IsEmpty) + { + AssociatedShpkDevkit = null; + LoadedShpkDevkitPathName = string.Empty; + } + else + { + AssociatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + } + + UpdateShaderKeys(); + _updateOnNextFrame = true; + } + + private void UpdateShaderKeys() + { + ShaderKeys.Clear(); + if (AssociatedShpk != null) + foreach (var key in AssociatedShpk.MaterialKeys) + { + var keyName = Names.KnownNames.TryResolve(key.Id); + var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var valueSet = new HashSet(key.Values); + if (dkData != null) + valueSet.UnionWith(dkData.Values.Keys); + + var valueKnownNames = keyName.WithKnownSuffixes(); + + var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); + var values = valueSet.Select(value => + { + var valueName = valueKnownNames.TryResolve(Names.KnownNames, value); + if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) + return (dkValue.Label.Length > 0 ? dkValue.Label : valueName.ToString(), value, dkValue.Description); + + return (valueName.ToString(), value, string.Empty); + }).ToArray(); + Array.Sort(values, (x, y) => + { + if (x.Value == key.DefaultValue) + return -1; + if (y.Value == key.DefaultValue) + return 1; + + return string.Compare(x.Label, y.Label, StringComparison.Ordinal); + }); + ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, + !hasDkLabel, values)); + } + else + foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) + { + var keyName = Names.KnownNames.TryResolve(key.Category); + var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); + ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); + } + } + + private void UpdateShaders() + { + static void AddShader(HashSet globalSet, Dictionary> byPassSets, uint passId, int shaderIndex) + { + globalSet.Add(shaderIndex); + if (!byPassSets.TryGetValue(passId, out var passSet)) + { + passSet = []; + byPassSets.Add(passId, passSet); + } + passSet.Add(shaderIndex); + } + + VertexShaders.Clear(); + PixelShaders.Clear(); + + var vertexShadersByPass = new Dictionary>(); + var pixelShadersByPass = new Dictionary>(); + + if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible()) + { + ShadersKnown = false; + } + else + { + ShadersKnown = true; + var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + var materialKeySelector = + BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + + foreach (var systemKeySelector in systemKeySelectors) + { + foreach (var sceneKeySelector in sceneKeySelectors) + { + foreach (var subViewKeySelector in subViewKeySelectors) + { + var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); + var node = AssociatedShpk.GetNodeBySelector(selector); + if (node.HasValue) + foreach (var pass in node.Value.Passes) + { + AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); + AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); + } + else + ShadersKnown = false; + } + } + } + } + + if (ShadersKnown) + { + var builder = new StringBuilder(); + foreach (var (passId, passVS) in vertexShadersByPass) + { + if (builder.Length > 0) + builder.Append("\n\n"); + + var passName = Names.KnownNames.TryResolve(passId); + var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}"); + if (pixelShadersByPass.TryGetValue(passId, out var passPS)) + { + shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}"); + } + } + foreach (var (passId, passPS) in pixelShadersByPass) + { + if (vertexShadersByPass.ContainsKey(passId)) + continue; + + if (builder.Length > 0) + builder.Append("\n\n"); + + var passName = Names.KnownNames.TryResolve(passId); + var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}"); + } + + ShadersString = Encoding.UTF8.GetBytes(builder.ToString()); + } + else + ShadersString = UnknownShadersString; + + ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + } + + private bool DrawShaderSection(bool disabled) + { + var ret = false; + if (ImGui.CollapsingHeader(ShaderHeader)) + { + ret |= DrawPackageNameInput(disabled); + ret |= DrawShaderFlagsInput(disabled); + DrawCustomAssociations(); + ret |= DrawMaterialShaderKeys(disabled); + DrawMaterialShaders(); + } + + if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null)) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + if (AssociatedShpk == null) + { + ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8, + ImGuiUtil.HalfBlendText(0x80u)); // Half red + } + else + { + ImUtf8.Text("No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."u8, + ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow + } + } + + return ret; + } + + private bool DrawPackageNameInput(bool disabled) + { + if (disabled) + { + ImGui.TextUnformatted("Shader Package: " + Mtrl.ShaderPackage.Name); + return false; + } + + var ret = false; + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using var c = ImRaii.Combo("Shader Package", Mtrl.ShaderPackage.Name); + if (c) + foreach (var value in GetShpkNames()) + { + if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) + { + Mtrl.ShaderPackage.Name = value; + ret = true; + AssociatedShpk = null; + LoadedShpkPath = FullPath.Empty; + LoadShpk(FindAssociatedShpk(out _, out _)); + } + } + + return ret; + } + + private bool DrawShaderFlagsInput(bool disabled) + { + 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))) + return false; + + Mtrl.ShaderPackage.Flags = (uint)shpkFlags; + SetShaderPackageFlags((uint)shpkFlags); + return true; + } + + /// + /// Show the currently associated shpk file, if any, and the buttons to associate + /// a specific shpk from your drive, the modded shpk by path or the default shpk. + /// + private void DrawCustomAssociations() + { + const string tooltip = "Click to copy file path to clipboard."; + var text = AssociatedShpk == null + ? "Associated .shpk file: None" + : $"Associated .shpk file: {LoadedShpkPathName}"; + var devkitText = AssociatedShpkDevkit == null + ? "Associated dev-kit file: None" + : $"Associated dev-kit file: {LoadedShpkDevkitPathName}"; + var baseDevkitText = AssociatedBaseDevkit == null + ? "Base dev-kit file: None" + : $"Base dev-kit file: {LoadedBaseDevkitPathName}"; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip); + ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip); + + if (ImGui.Button("Associate Custom .shpk File")) + _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => + { + if (success) + LoadShpk(new FullPath(name[0])); + }, 1, _edit.Mod!.ModPath.FullName, false); + + var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), + moddedPath.Equals(LoadedShpkPath))) + LoadShpk(moddedPath); + + if (!gamePath.Path.Equals(moddedPath.InternalName)) + { + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, + gamePath.Path.Equals(LoadedShpkPath.InternalName))) + LoadShpk(new FullPath(gamePath)); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + } + + private bool DrawMaterialShaderKeys(bool disabled) + { + if (ShaderKeys.Count == 0) + return false; + + var ret = false; + foreach (var (label, index, description, monoFont, values) in ShaderKeys) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); + ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; + var shpkKey = AssociatedShpk?.GetMaterialKeyById(key.Category); + var currentValue = key.Value; + var (currentLabel, _, currentDescription) = + values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); + if (!disabled && shpkKey.HasValue) + { + ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); + using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + { + if (c) + foreach (var (valueLabel, value, valueDescription) in values) + { + if (ImGui.Selectable(valueLabel, value == currentValue)) + { + key.Value = value; + ret = true; + Update(); + } + + if (valueDescription.Length > 0) + ImGuiUtil.SelectableHelpMarker(valueDescription); + } + } + + ImGui.SameLine(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + else if (description.Length > 0 || currentDescription.Length > 0) + { + ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); + } + else + { + ImGui.TextUnformatted($"{label}: {currentLabel}"); + } + } + + return ret; + } + + private void DrawMaterialShaders() + { + if (AssociatedShpk == null) + return; + + using (var node = ImUtf8.TreeNode("Candidate Shaders"u8)) + { + if (node) + ImUtf8.Text(ShadersString.Span); + } + + if (ShaderComment.Length > 0) + { + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ImGui.TextUnformatted(ShaderComment); + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs new file mode 100644 index 00000000..3181dafe --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -0,0 +1,276 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.String.Classes; +using static Penumbra.GameData.Files.MaterialStructs.SamplerFlags; +using static Penumbra.GameData.Files.ShpkFile; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); + + public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet SamplerIds = new(16); + public float TextureLabelWidth; + + private void UpdateTextures() + { + Textures.Clear(); + SamplerIds.Clear(); + if (AssociatedShpk == null) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + SamplerIds.Add(TableSamplerId); + + foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) + Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); + } + else + { + foreach (var index in VertexShaders) + SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in PixelShaders) + SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!ShadersKnown) + { + SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + SamplerIds.Add(TableSamplerId); + } + + foreach (var samplerId in SamplerIds) + { + var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + if (shpkSampler is not { Slot: 2 }) + continue; + + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); + + var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + dkData?.Description ?? string.Empty, !hasDkLabel)); + } + + if (SamplerIds.Contains(TableSamplerId)) + Mtrl.Table ??= new ColorTable(); + } + + Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); + + TextureLabelWidth = 50f * UiHelpers.Scale; + + float helpWidth; + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { + helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; + } + + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (!monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + { + foreach (var (label, _, _, description, monoFont) in Textures) + { + if (monoFont) + TextureLabelWidth = Math.Max(TextureLabelWidth, + ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); + } + } + + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; + } + + private static ReadOnlySpan TextureAddressModeTooltip(TextureAddressMode addressMode) + => addressMode switch + { + TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, + TextureAddressMode.Mirror => "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on."u8, + TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, + TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8, + _ => ""u8, + }; + + private bool DrawTextureSection(bool disabled) + { + if (Textures.Count == 0) + return false; + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) + return false; + + var frameHeight = ImGui.GetFrameHeight(); + var ret = false; + using var table = ImRaii.Table("##Textures", 3); + + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, TextureLabelWidth * UiHelpers.Scale); + foreach (var (label, textureI, samplerI, description, monoFont) in Textures) + { + using var _ = ImRaii.PushId(samplerI); + var tmp = Mtrl.Textures[textureI].Path; + var unfolded = UnfoldedTextures.Contains(samplerI); + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), + new Vector2(frameHeight), + "Settings for this texture and the associated sampler", false, true)) + { + unfolded = !unfolded; + if (unfolded) + UnfoldedTextures.Add(samplerI); + else + UnfoldedTextures.Remove(samplerI); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) + && tmp.Length > 0 + && tmp != Mtrl.Textures[textureI].Path) + { + ret = true; + Mtrl.Textures[textureI].Path = tmp; + } + + ImGui.TableNextColumn(); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); + } + + if (unfolded) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ret |= DrawMaterialSampler(disabled, textureI, samplerI); + ImGui.TableNextColumn(); + } + } + + return ret; + } + + private static bool ComboTextureAddressMode(ReadOnlySpan label, ref TextureAddressMode value) + { + using var c = ImUtf8.Combo(label, value.ToString()); + if (!c) + return false; + + var ret = false; + foreach (var mode in Enum.GetValues()) + { + if (ImGui.Selectable(mode.ToString(), mode == value)) + { + value = mode; + ret = true; + } + + ImUtf8.SelectableHelpMarker(TextureAddressModeTooltip(mode)); + } + + return ret; + } + + private bool DrawMaterialSampler(bool disabled, int textureIdx, int samplerIdx) + { + var ret = false; + ref var texture = ref Mtrl.Textures[textureIdx]; + ref var sampler = ref Mtrl.ShaderPackage.Samplers[samplerIdx]; + + var dx11 = texture.DX11; + if (ImUtf8.Checkbox("Prepend -- to the file name on DirectX 11"u8, ref dx11)) + { + texture.DX11 = dx11; + ret = true; + } + + ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + var addressMode = samplerFlags.UAddressMode; + if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + { + samplerFlags.UAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + addressMode = samplerFlags.VAddressMode; + if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + { + samplerFlags.VAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = samplerFlags.LodBias; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) + { + samplerFlags.LodBias = lodBias; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = samplerFlags.MinLod; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) + { + samplerFlags.MinLod = minLod; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); + + using var t = ImUtf8.TreeNode("Advanced Settings"u8); + if (!t) + return ret; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.InputScalar("Texture Flags"u8, ref texture.Flags, "%04X"u8, + flags: disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) + ret = true; + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.InputScalar("Sampler Flags"u8, ref sampler.Flags, "%08X"u8, + flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + { + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs new file mode 100644 index 00000000..2d4e93f1 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -0,0 +1,199 @@ +using Dalamud.Plugin.Services; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.GameData; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Interop; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.ResourceTree; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed partial class MtrlTab : IWritable, IDisposable +{ + private const int ShpkPrefixLength = 16; + + private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); + + private readonly IDataManager _gameData; + private readonly IFramework _framework; + private readonly ObjectManager _objects; + private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly StainService _stainService; + private readonly ResourceTreeFactory _resourceTreeFactory; + private readonly FileDialogService _fileDialog; + private readonly MaterialTemplatePickers _materialTemplatePickers; + private readonly Configuration _config; + + private readonly ModEditWindow _edit; + public readonly MtrlFile Mtrl; + public readonly string FilePath; + public readonly bool Writable; + + private bool _updateOnNextFrame; + + public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor, + StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, + Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable) + { + _gameData = gameData; + _framework = framework; + _objects = objects; + _characterBaseDestructor = characterBaseDestructor; + _stainService = stainService; + _resourceTreeFactory = resourceTreeFactory; + _fileDialog = fileDialog; + _materialTemplatePickers = materialTemplatePickers; + _config = config; + + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + Update(); + LoadShpk(FindAssociatedShpk(out _, out _)); + if (writable) + { + _characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); + BindToMaterialInstances(); + } + } + + public bool DrawVersionUpdate(bool disabled) + { + if (disabled || Mtrl.IsDawntrail) + return false; + + if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, + "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, + new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + return false; + + Mtrl.MigrateToDawntrail(); + Update(); + LoadShpk(FindAssociatedShpk(out _, out _)); + return true; + } + + public bool DrawPanel(bool disabled) + { + if (_updateOnNextFrame) + { + _updateOnNextFrame = false; + Update(); + } + + DrawMaterialLivePreviewRebind(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + var ret = DrawBackFaceAndTransparency(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + ret |= DrawShaderSection(disabled); + + ret |= DrawTextureSection(disabled); + ret |= DrawColorTableSection(disabled); + ret |= DrawConstantsSection(disabled); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawOtherMaterialDetails(disabled); + + return !disabled && ret; + } + + private bool DrawBackFaceAndTransparency(bool disabled) + { + ref var shaderFlags = ref ShaderFlags.Wrap(ref Mtrl.ShaderPackage.Flags); + + var ret = false; + + using var dis = ImRaii.Disabled(disabled); + + var tmp = shaderFlags.EnableTransparency; + if (ImGui.Checkbox("Enable Transparency", ref tmp)) + { + shaderFlags.EnableTransparency = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + + ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + tmp = shaderFlags.HideBackfaces; + if (ImGui.Checkbox("Hide Backfaces", ref tmp)) + { + shaderFlags.HideBackfaces = tmp; + ret = true; + SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); + } + + if (ShpkLoading) + { + ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); + + ImUtf8.Text("Loading shader (.shpk) file. Some functionality will only be available after this is done."u8, + ImGuiUtil.HalfBlendText(0x808000u)); // Half cyan + } + + return ret; + } + + private void DrawOtherMaterialDetails(bool _) + { + if (!ImGui.CollapsingHeader("Further Content")) + return; + + using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in Mtrl.UvSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (sets) + foreach (var set in Mtrl.ColorSets) + ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + if (Mtrl.AdditionalData.Length <= 0) + return; + + using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); + if (t) + Widget.DrawHexViewer(Mtrl.AdditionalData); + } + + public void Update() + { + UpdateShaders(); + UpdateTextures(); + UpdateConstants(); + } + + public unsafe void Dispose() + { + UnbindFromMaterialInstances(); + if (Writable) + _characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); + } + + public bool Valid + => ShadersKnown && Mtrl.Valid; + + public byte[] Write() + { + var output = Mtrl.Clone(); + output.GarbageCollect(AssociatedShpk, SamplerIds); + + return output.Write(); + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs new file mode 100644 index 00000000..af8b7db2 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -0,0 +1,18 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Files; +using Penumbra.GameData.Interop; +using Penumbra.Interop.Hooks.Objects; +using Penumbra.Interop.ResourceTree; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects, + CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory, + FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService +{ + public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable) + => new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog, + materialTemplatePickers, config, edit, file, filePath, writable); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs deleted file mode 100644 index 25c0e448..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.String.Functions; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private static readonly float HalfMinValue = (float)Half.MinValue; - private static readonly float HalfMaxValue = (float)Half.MaxValue; - private static readonly float HalfEpsilon = (float)Half.Epsilon; - - private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled) - { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - ColorTableCopyAllClipboardButton(tab.Mtrl); - ImGui.SameLine(); - var ret = ColorTablePasteAllClipboardButton(tab, disabled); - if (!disabled) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= ColorTableDyeableCheckbox(tab); - } - - var hasDyeTable = tab.Mtrl.HasDyeTable; - if (hasDyeTable) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= DrawPreviewDye(tab, disabled); - } - - using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); - if (!table) - return false; - - ImGui.TableNextColumn(); - ImGui.TableHeader(string.Empty); - ImGui.TableNextColumn(); - ImGui.TableHeader("Row"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Diffuse"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Specular"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Emissive"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Gloss"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Tile"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Repeat"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Skew"); - if (hasDyeTable) - { - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye Preview"); - } - - for (var i = 0; i < ColorTable.NumRows; ++i) - { - ret |= DrawColorTableRow(tab, i, disabled); - ImGui.TableNextRow(); - } - - return ret; - } - - - private static void ColorTableCopyAllClipboardButton(MtrlFile file) - { - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) - return; - - try - { - var data1 = file.Table.AsBytes(); - var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan.Empty; - var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo(array); - data2.TryCopyTo(array.AsSpan(data1.Length)); - var text = Convert.ToBase64String(array); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private bool DrawPreviewDye(MtrlTab tab, bool disabled) - { - var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; - var tt = dyeId == 0 - ? "Select a preview dye first." - : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) - { - var ret = false; - if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); - - tab.UpdateColorTablePreview(); - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorTablePreview(); - return false; - } - - private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || !tab.Mtrl.HasTable) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) - return false; - - ref var rows = ref tab.Mtrl.Table; - fixed (void* ptr = data, output = &rows) - { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.HasDyeTable) - { - ref var dyeRows = ref tab.Mtrl.DyeTable; - fixed (void* output2 = &dyeRows) - { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); - } - } - } - - tab.UpdateColorTablePreview(); - - return true; - } - catch - { - return false; - } - } - - [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true)) - return; - - try - { - Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; - fixed (byte* ptr = data) - { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); - } - - var text = Convert.ToBase64String(data); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { - var dyeable = tab.Mtrl.HasDyeTable; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); - - if (ret) - { - tab.Mtrl.HasDyeTable = dyeable; - tab.UpdateColorTablePreview(); - } - - return ret; - } - - private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true)) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size - || !tab.Mtrl.HasTable) - return false; - - fixed (byte* ptr = data) - { - tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; - if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); - } - - tab.UpdateColorTableRowPreview(rowIdx); - - return true; - } - catch - { - return false; - } - } - - private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled) - { - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true); - - if (ImGui.IsItemHovered()) - tab.HighlightColorTableRow(rowIdx); - else if (tab.HighlightedColorTableRow == rowIdx) - tab.CancelColorTableHighlight(); - } - - private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) - { - static bool FixFloat(ref float val, float current) - { - val = (float)(Half)val; - return val != current; - } - - using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; - var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; - var floatSize = 70 * UiHelpers.Scale; - var intSize = 45 * UiHelpers.Scale; - ImGui.TableNextColumn(); - ColorTableCopyClipboardButton(row, dye); - ImGui.SameLine(); - var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); - ImGui.SameLine(); - ColorTableHighlightButton(tab, rowIdx, disabled); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); - - ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled(disabled); - ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => - { - tab.Mtrl.Table[rowIdx].Diffuse = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => - { - tab.Mtrl.DyeTable[rowIdx].Diffuse = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => - { - tab.Mtrl.Table[rowIdx].Specular = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.SpecularStrength)) - { - row.SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => - { - tab.Mtrl.DyeTable[rowIdx].Specular = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => - { - tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => - { - tab.Mtrl.Table[rowIdx].Emissive = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => - { - tab.Mtrl.DyeTable[rowIdx].Emissive = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth(floatSize); - var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") - && FixFloat(ref tmpFloat, row.GlossStrength)) - { - row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => - { - tab.Mtrl.DyeTable[rowIdx].Gloss = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - int tmpInt = row.TileSet; - ImGui.SetNextItemWidth(intSize); - if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) - { - row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) - { - row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) - { - row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) - { - row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.SameLine(); - tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) - { - row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) - { - dye.Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); - } - - - return ret; - } - - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) - { - var stain = _stainService.StainCombo.CurrentSelection.Key; - if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) - return false; - - var values = entry[(int)stain]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - - var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Apply the selected dye to this row.", disabled, true); - - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); - if (ret) - tab.UpdateColorTableRowPreview(rowIdx); - - ImGui.SameLine(); - ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); - ImGui.SameLine(); - ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); - ImGui.SameLine(); - ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); - ImGui.SameLine(); - using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); - - return ret; - } - - private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") - { - var ret = false; - var inputSqrt = PseudoSqrtRgb(input); - var tmp = inputSqrt; - if (ImGui.ColorEdit3(label, ref tmp, - ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB - | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR) - && tmp != inputSqrt) - { - setter(PseudoSquareRgb(tmp)); - ret = true; - } - - if (letter.Length > 0 && ImGui.IsItemVisible()) - { - var textSize = ImGui.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; - var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText(center, textColor, letter); - } - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return ret; - } - - // Functions to deal with squared RGB values without making negatives useless. - - private static float PseudoSquareRgb(float x) - => x < 0.0f ? -(x * x) : x * x; - - private static Vector3 PseudoSquareRgb(Vector3 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); - - private static Vector4 PseudoSquareRgb(Vector4 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); - - private static float PseudoSqrtRgb(float x) - => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - - internal static Vector3 PseudoSqrtRgb(Vector3 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); - - private static Vector4 PseudoSqrtRgb(Vector4 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs deleted file mode 100644 index 1f5db38e..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ConstantEditor.cs +++ /dev/null @@ -1,247 +0,0 @@ -using ImGuiNET; -using OtterGui.Raii; -using OtterGui; -using Penumbra.GameData; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private interface IConstantEditor - { - bool Draw(Span values, bool disabled); - } - - private sealed class FloatConstantEditor : IConstantEditor - { - public static readonly FloatConstantEditor Default = new(null, null, 0.1f, 0.0f, 1.0f, 0.0f, 3, string.Empty); - - private readonly float? _minimum; - private readonly float? _maximum; - private readonly float _speed; - private readonly float _relativeSpeed; - private readonly float _factor; - private readonly float _bias; - private readonly string _format; - - public FloatConstantEditor(float? minimum, float? maximum, float speed, float relativeSpeed, float factor, float bias, byte precision, - string unit) - { - _minimum = minimum; - _maximum = maximum; - _speed = speed; - _relativeSpeed = relativeSpeed; - _factor = factor; - _bias = bias; - _format = $"%.{Math.Min(precision, (byte)9)}f"; - if (unit.Length > 0) - _format = $"{_format} {unit.Replace("%", "%%")}"; - } - - public bool Draw(Span values, bool disabled) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var value = (values[valueIdx] - _bias) / _factor; - if (disabled) - { - ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); - } - else - { - if (ImGui.DragFloat($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0.0f, - _maximum ?? 0.0f, _format)) - { - values[valueIdx] = Clamp(value) * _factor + _bias; - ret = true; - } - } - } - - return ret; - } - - private float Clamp(float value) - => Math.Clamp(value, _minimum ?? float.NegativeInfinity, _maximum ?? float.PositiveInfinity); - } - - private sealed class IntConstantEditor : IConstantEditor - { - private readonly int? _minimum; - private readonly int? _maximum; - private readonly float _speed; - private readonly float _relativeSpeed; - private readonly float _factor; - private readonly float _bias; - private readonly string _format; - - public IntConstantEditor(int? minimum, int? maximum, float speed, float relativeSpeed, float factor, float bias, string unit) - { - _minimum = minimum; - _maximum = maximum; - _speed = speed; - _relativeSpeed = relativeSpeed; - _factor = factor; - _bias = bias; - _format = "%d"; - if (unit.Length > 0) - _format = $"{_format} {unit.Replace("%", "%%")}"; - } - - public bool Draw(Span values, bool disabled) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - // Not using DragScalarN because of _relativeSpeed and other points of lost flexibility. - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var value = (int)Math.Clamp(MathF.Round((values[valueIdx] - _bias) / _factor), int.MinValue, int.MaxValue); - if (disabled) - { - ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), value, value, _format); - } - else - { - if (ImGui.DragInt($"##{valueIdx}", ref value, Math.Max(_speed, value * _relativeSpeed), _minimum ?? 0, _maximum ?? 0, - _format)) - { - values[valueIdx] = Clamp(value) * _factor + _bias; - ret = true; - } - } - } - - return ret; - } - - private int Clamp(int value) - => Math.Clamp(value, _minimum ?? int.MinValue, _maximum ?? int.MaxValue); - } - - private sealed class ColorConstantEditor : IConstantEditor - { - private readonly bool _squaredRgb; - private readonly bool _clamped; - - public ColorConstantEditor(bool squaredRgb, bool clamped) - { - _squaredRgb = squaredRgb; - _clamped = clamped; - } - - public bool Draw(Span values, bool disabled) - { - switch (values.Length) - { - case 3: - { - var value = new Vector3(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (!ImGui.ColorEdit3("##0", ref value, ImGuiColorEditFlags.Float | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) || disabled) - return false; - - if (_squaredRgb) - value = PseudoSquareRgb(value); - if (_clamped) - value = Vector3.Clamp(value, Vector3.Zero, Vector3.One); - value.CopyTo(values); - return true; - } - case 4: - { - var value = new Vector4(values); - if (_squaredRgb) - value = PseudoSqrtRgb(value); - if (!ImGui.ColorEdit4("##0", ref value, - ImGuiColorEditFlags.Float | ImGuiColorEditFlags.AlphaPreviewHalf | (_clamped ? 0 : ImGuiColorEditFlags.HDR)) - || disabled) - return false; - - if (_squaredRgb) - value = PseudoSquareRgb(value); - if (_clamped) - value = Vector4.Clamp(value, Vector4.Zero, Vector4.One); - value.CopyTo(values); - return true; - } - default: return FloatConstantEditor.Default.Draw(values, disabled); - } - } - } - - private sealed class EnumConstantEditor : IConstantEditor - { - private readonly IReadOnlyList<(string Label, float Value, string Description)> _values; - - public EnumConstantEditor(IReadOnlyList<(string Label, float Value, string Description)> values) - => _values = values; - - public bool Draw(Span values, bool disabled) - { - var spacing = ImGui.GetStyle().ItemInnerSpacing.X; - var fieldWidth = (ImGui.CalcItemWidth() - (values.Length - 1) * spacing) / values.Length; - - var ret = false; - - for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) - { - using var id = ImRaii.PushId(valueIdx); - if (valueIdx > 0) - ImGui.SameLine(0.0f, spacing); - - ImGui.SetNextItemWidth(MathF.Round(fieldWidth * (valueIdx + 1)) - MathF.Round(fieldWidth * valueIdx)); - - var currentValue = values[valueIdx]; - var currentLabel = _values.FirstOrNull(v => v.Value == currentValue)?.Label - ?? currentValue.ToString(CultureInfo.CurrentCulture); - ret = disabled - ? ImGui.InputText(string.Empty, ref currentLabel, (uint)currentLabel.Length, ImGuiInputTextFlags.ReadOnly) - : DrawCombo(currentLabel, ref values[valueIdx]); - } - - return ret; - } - - private bool DrawCombo(string label, ref float currentValue) - { - using var c = ImRaii.Combo(string.Empty, label); - if (!c) - return false; - - var ret = false; - foreach (var (valueLabel, value, valueDescription) in _values) - { - if (ImGui.Selectable(valueLabel, value == currentValue)) - { - currentValue = value; - ret = true; - } - - if (valueDescription.Length > 0) - ImGuiUtil.SelectableHelpMarker(valueDescription); - } - - return ret; - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs deleted file mode 100644 index 29fd7531..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ /dev/null @@ -1,783 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.Objects; -using Penumbra.Interop.MaterialPreview; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; -using static Penumbra.GameData.Files.ShpkFile; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private sealed class MtrlTab : IWritable, IDisposable - { - private const int ShpkPrefixLength = 16; - - private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - - private readonly ModEditWindow _edit; - public readonly MtrlFile Mtrl; - public readonly string FilePath; - public readonly bool Writable; - - private string[]? _shpkNames; - - public string ShaderHeader = "Shader###Shader"; - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public string LoadedShpkDevkitPathName = string.Empty; - public string ShaderComment = string.Empty; - public ShpkFile? AssociatedShpk; - public JObject? AssociatedShpkDevkit; - - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; - - // Shader Key State - public readonly - List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); - - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public string VertexShadersString = "Vertex Shaders: ???"; - public string PixelShadersString = "Pixel Shaders: ???"; - - // Textures & Samplers - public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); - - public readonly HashSet UnfoldedTextures = new(4); - public readonly HashSet SamplerIds = new(16); - public float TextureLabelWidth; - - // Material Constants - public readonly - List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> - Constants)> Constants = new(16); - - // Live-Previewers - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTableRow = -1; - public readonly Stopwatch HighlightTime = new(); - - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) - { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) - return FullPath.Empty; - - return _edit.FindBestMatch(defaultGamePath); - } - - public string[] GetShpkNames() - { - if (null != _shpkNames) - return _shpkNames; - - var names = new HashSet(StandardShaderPackages); - names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); - - _shpkNames = names.ToArray(); - Array.Sort(_shpkNames); - - return _shpkNames; - } - - public void LoadShpk(FullPath path) - { - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - - try - { - LoadedShpkPath = path; - var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); - } - catch (Exception e) - { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); - } - - if (LoadedShpkPath.InternalName.IsEmpty) - { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; - } - else - { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); - } - - UpdateShaderKeys(); - Update(); - } - - private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) - { - try - { - if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) - throw new Exception("Could not assemble ShPk dev-kit path."); - - var devkitFullPath = _edit.FindBestMatch(devkitPath); - if (!devkitFullPath.IsRooted) - throw new Exception("Could not resolve ShPk dev-kit path."); - - devkitPathName = devkitFullPath.FullName; - return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); - } - catch - { - devkitPathName = string.Empty; - return null; - } - } - - private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); - - private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class - { - if (devkit == null) - return null; - - try - { - var data = devkit[category]; - if (id.HasValue) - data = data?[id.Value.ToString()]; - - if (mayVary && (data as JObject)?["Vary"] != null) - { - var selector = BuildSelector(data!["Vary"]! - .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); - var index = (int)data["Selectors"]![selector.ToString()]!; - data = data["Items"]![index]; - } - - return data?.ToObject(typeof(T)) as T; - } - catch (Exception e) - { - // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) - Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); - return null; - } - } - - private void UpdateShaderKeys() - { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) - { - var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var valueSet = new HashSet(key.Values); - if (dkData != null) - valueSet.UnionWith(dkData.Values.Keys); - - var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); - var values = valueSet.Select(value => - { - if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) - return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); - - return ($"0x{value:X8}", value, string.Empty); - }).ToArray(); - Array.Sort(values, (x, y) => - { - if (x.Value == key.DefaultValue) - return -1; - if (y.Value == key.DefaultValue) - return 1; - - return string.Compare(x.Label, y.Label, StringComparison.Ordinal); - }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, - !hasDkLabel, values)); - } - else - foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) - ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); - } - - private void UpdateShaders() - { - VertexShaders.Clear(); - PixelShaders.Clear(); - if (AssociatedShpk == null) - { - ShadersKnown = false; - } - else - { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); - var materialKeySelector = - BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); - foreach (var systemKeySelector in systemKeySelectors) - { - foreach (var sceneKeySelector in sceneKeySelectors) - { - foreach (var subViewKeySelector in subViewKeySelectors) - { - var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); - if (node.HasValue) - foreach (var pass in node.Value.Passes) - { - VertexShaders.Add((int)pass.VertexShader); - PixelShaders.Add((int)pass.PixelShader); - } - else - ShadersKnown = false; - } - } - } - } - - var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); - var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); - - VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; - PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; - - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; - } - - private void UpdateTextures() - { - Textures.Clear(); - SamplerIds.Clear(); - if (AssociatedShpk == null) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - - foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) - Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); - } - else - { - foreach (var index in VertexShaders) - SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in PixelShaders) - SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!ShadersKnown) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - } - - foreach (var samplerId in SamplerIds) - { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) - continue; - - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, - dkData?.Description ?? string.Empty, !hasDkLabel)); - } - - if (SamplerIds.Contains(TableSamplerId)) - Mtrl.HasTable = true; - } - - Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - - TextureLabelWidth = 50f * UiHelpers.Scale; - - float helpWidth; - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; - } - - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (!monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) - { - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, - ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - } - - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; - } - - private void UpdateConstants() - { - static List FindOrAddGroup(List<(string, List)> groups, string name) - { - foreach (var (groupName, group) in groups) - { - if (string.Equals(name, groupName, StringComparison.Ordinal)) - return group; - } - - var newGroup = new List(16); - groups.Add((name, newGroup)); - return newGroup; - } - - Constants.Clear(); - if (AssociatedShpk == null) - { - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) - { - var values = Mtrl.GetConstantValues(constant); - for (var i = 0; i < values.Length; i += 4) - { - fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - else - { - var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; - foreach (var shpkConstant in AssociatedShpk.MaterialParams) - { - if ((shpkConstant.ByteSize & 0x3) != 0) - continue; - - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); - var values = Mtrl.GetConstantValues(constant); - var handledElements = new IndexSet(values.Length, false); - - var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); - if (dkData != null) - foreach (var dkConstant in dkData) - { - var offset = (int)dkConstant.Offset; - var length = values.Length - offset; - if (dkConstant.Length.HasValue) - length = Math.Min(length, (int)dkConstant.Length.Value); - if (length <= 0) - continue; - - var editor = dkConstant.CreateEditor(); - if (editor != null) - FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") - .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); - handledElements.AddRange(offset, length); - } - - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(complement:true)) - { - if ((shpkConstant.ByteOffset & 0x3) == 0) - { - var offset = shpkConstant.ByteOffset >> 2; - for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) - { - var rangeStart = Math.Max(i, start); - var rangeEnd = Math.Min(i + 4, end); - if (rangeEnd > rangeStart) - fcGroup.Add(( - $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", - constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); - } - } - else - { - for (var i = start; i < end; i += 4) - { - fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - } - } - - Constants.RemoveAll(group => group.Constants.Count == 0); - Constants.Sort((x, y) => - { - if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) - return 1; - if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) - return -1; - - return string.Compare(x.Header, y.Header, StringComparison.Ordinal); - }); - // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme - foreach (var (_, group) in Constants) - { - group.Sort((x, y) => string.CompareOrdinal( - x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, - y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); - } - } - - public unsafe void BindToMaterialInstances() - { - UnbindFromMaterialInstances(); - - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), - FilePath); - - var foundMaterials = new HashSet(); - foreach (var materialInfo in instances) - { - var material = materialInfo.GetDrawObjectMaterial(_edit._objects); - if (foundMaterials.Contains((nint)material)) - continue; - - try - { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); - foundMaterials.Add((nint)material); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) - return; - - foreach (var materialInfo in instances) - { - try - { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateColorTablePreview(); - } - - private void UnbindFromMaterialInstances() - { - foreach (var previewer in MaterialPreviewers) - previewer.Dispose(); - MaterialPreviewers.Clear(); - - foreach (var previewer in ColorTablePreviewers) - previewer.Dispose(); - ColorTablePreviewers.Clear(); - } - - private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) - { - for (var i = MaterialPreviewers.Count; i-- > 0;) - { - var previewer = MaterialPreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); - } - - for (var i = ColorTablePreviewers.Count; i-- > 0;) - { - var previewer = ColorTablePreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); - } - } - - public void SetShaderPackageFlags(uint shPkFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetShaderPackageFlags(shPkFlags); - } - - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetMaterialParameter(parameterCrc, offset, value); - } - - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetSamplerFlags(samplerCrc, samplerFlags); - } - - private void UpdateMaterialPreview() - { - SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); - foreach (var constant in Mtrl.ShaderPackage.Constants) - { - var values = Mtrl.GetConstantValues(constant); - if (values != null) - SetMaterialParameter(constant.Id, 0, values); - } - - foreach (var sampler in Mtrl.ShaderPackage.Samplers) - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - public void HighlightColorTableRow(int rowIdx) - { - var oldRowIdx = HighlightedColorTableRow; - - if (HighlightedColorTableRow != rowIdx) - { - HighlightedColorTableRow = rowIdx; - HighlightTime.Restart(); - } - - if (oldRowIdx >= 0) - UpdateColorTableRowPreview(oldRowIdx); - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void CancelColorTableHighlight() - { - var rowIdx = HighlightedColorTableRow; - - HighlightedColorTableRow = -1; - HighlightTime.Reset(); - - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void UpdateColorTableRowPreview(int rowIdx) - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); - if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - - if (HighlightedColorTableRow == rowIdx) - ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() - .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); - previewer.ScheduleUpdate(); - } - } - - public void UpdateColorTablePreview() - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var rows = new LegacyColorTable(Mtrl.Table); - var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - { - ref var row = ref rows[i]; - var dye = dyeRows[i]; - if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - } - - if (HighlightedColorTableRow >= 0) - ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - // TODO: Dawntrail - rows.AsHalves().CopyTo(previewer.ColorTable); - previewer.ScheduleUpdate(); - } - } - - private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) - { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = ColorId.InGameHighlight.Value(); - var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); - - row.Diffuse = Vector3.Zero; - row.Specular = Vector3.Zero; - row.Emissive = color * color; - } - - public void Update() - { - UpdateShaders(); - UpdateTextures(); - UpdateConstants(); - } - - public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) - { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); - LoadShpk(FindAssociatedShpk(out _, out _)); - if (writable) - { - _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); - BindToMaterialInstances(); - } - } - - public unsafe void Dispose() - { - UnbindFromMaterialInstances(); - if (Writable) - _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); - } - - // TODO Readd ShadersKnown - public bool Valid - => (true || ShadersKnown) && Mtrl.Valid; - - public byte[] Write() - { - var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); - - return output.Write(); - } - - private sealed class DevkitShaderKeyValue - { - public string Label = string.Empty; - public string Description = string.Empty; - } - - private sealed class DevkitShaderKey - { - public string Label = string.Empty; - public string Description = string.Empty; - public Dictionary Values = new(); - } - - private sealed class DevkitSampler - { - public string Label = string.Empty; - public string Description = string.Empty; - public string DefaultTexture = string.Empty; - } - - private enum DevkitConstantType - { - Hidden = -1, - Float = 0, - Integer = 1, - Color = 2, - Enum = 3, - } - - private sealed class DevkitConstantValue - { - public string Label = string.Empty; - public string Description = string.Empty; - public float Value = 0; - } - - private sealed class DevkitConstant - { - public uint Offset = 0; - public uint? Length = null; - public string Group = string.Empty; - public string Label = string.Empty; - public string Description = string.Empty; - public DevkitConstantType Type = DevkitConstantType.Float; - - public float? Minimum = null; - public float? Maximum = null; - public float? Speed = null; - public float RelativeSpeed = 0.0f; - public float Factor = 1.0f; - public float Bias = 0.0f; - public byte Precision = 3; - public string Unit = string.Empty; - - public bool SquaredRgb = false; - public bool Clamped = false; - - public DevkitConstantValue[] Values = Array.Empty(); - - public IConstantEditor? CreateEditor() - => Type switch - { - DevkitConstantType.Hidden => null, - DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, - Unit), - DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, - Factor, Bias, Unit), - DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), - DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, - value => (value.Label, value.Value, value.Description))), - _ => FloatConstantEditor.Default, - }; - - private static int? ToInteger(float? value) - => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs deleted file mode 100644 index b9525b29..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ /dev/null @@ -1,481 +0,0 @@ -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData; -using Penumbra.String.Classes; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private readonly FileDialogService _fileDialog; - - // strings path/to/the.exe | grep --fixed-strings '.shpk' | sort -u | sed -e 's#^shader/sm5/shpk/##' - // Apricot shader packages are unlisted because - // 1. they cause performance/memory issues when calculating the effective shader set - // 2. they probably aren't intended for use with materials anyway - private static readonly IReadOnlyList StandardShaderPackages = new[] - { - "3dui.shpk", - // "apricot_decal_dummy.shpk", - // "apricot_decal_ring.shpk", - // "apricot_decal.shpk", - // "apricot_lightmodel.shpk", - // "apricot_model_dummy.shpk", - // "apricot_model_morph.shpk", - // "apricot_model.shpk", - // "apricot_powder_dummy.shpk", - // "apricot_powder.shpk", - // "apricot_shape_dummy.shpk", - // "apricot_shape.shpk", - "bgcolorchange.shpk", - "bgcrestchange.shpk", - "bgdecal.shpk", - "bg.shpk", - "bguvscroll.shpk", - "channeling.shpk", - "characterglass.shpk", - "charactershadowoffset.shpk", - "character.shpk", - "cloud.shpk", - "createviewposition.shpk", - "crystal.shpk", - "directionallighting.shpk", - "directionalshadow.shpk", - "grass.shpk", - "hair.shpk", - "iris.shpk", - "lightshaft.shpk", - "linelighting.shpk", - "planelighting.shpk", - "pointlighting.shpk", - "river.shpk", - "shadowmask.shpk", - "skin.shpk", - "spotlighting.shpk", - "verticalfog.shpk", - "water.shpk", - "weather.shpk", - }; - - private enum TextureAddressMode : uint - { - Wrap = 0, - Mirror = 1, - Clamp = 2, - Border = 3, - } - - private static readonly IReadOnlyList TextureAddressModeTooltips = new[] - { - "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times.", - "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on.", - "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively.", - "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black).", - }; - - private static bool DrawPackageNameInput(MtrlTab tab, bool disabled) - { - if (disabled) - { - ImGui.TextUnformatted("Shader Package: " + tab.Mtrl.ShaderPackage.Name); - return false; - } - - var ret = false; - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using var c = ImRaii.Combo("Shader Package", tab.Mtrl.ShaderPackage.Name); - if (c) - foreach (var value in tab.GetShpkNames()) - { - if (ImGui.Selectable(value, value == tab.Mtrl.ShaderPackage.Name)) - { - tab.Mtrl.ShaderPackage.Name = value; - ret = true; - tab.AssociatedShpk = null; - tab.LoadedShpkPath = FullPath.Empty; - tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); - } - } - - return ret; - } - - private static bool DrawShaderFlagsInput(MtrlTab tab, bool disabled) - { - var shpkFlags = (int)tab.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))) - return false; - - tab.Mtrl.ShaderPackage.Flags = (uint)shpkFlags; - tab.SetShaderPackageFlags((uint)shpkFlags); - return true; - } - - /// - /// Show the currently associated shpk file, if any, and the buttons to associate - /// a specific shpk from your drive, the modded shpk by path or the default shpk. - /// - private void DrawCustomAssociations(MtrlTab tab) - { - const string tooltip = "Click to copy file path to clipboard."; - var text = tab.AssociatedShpk == null - ? "Associated .shpk file: None" - : $"Associated .shpk file: {tab.LoadedShpkPathName}"; - var devkitText = tab.AssociatedShpkDevkit == null - ? "Associated dev-kit file: None" - : $"Associated dev-kit file: {tab.LoadedShpkDevkitPathName}"; - var baseDevkitText = tab.AssociatedBaseDevkit == null - ? "Base dev-kit file: None" - : $"Base dev-kit file: {tab.LoadedBaseDevkitPathName}"; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - - ImGuiUtil.CopyOnClickSelectable(text, tab.LoadedShpkPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(devkitText, tab.LoadedShpkDevkitPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(baseDevkitText, tab.LoadedBaseDevkitPathName, tooltip); - - if (ImGui.Button("Associate Custom .shpk File")) - _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => - { - if (success) - tab.LoadShpk(new FullPath(name[0])); - }, 1, Mod!.ModPath.FullName, false); - - var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), - moddedPath.Equals(tab.LoadedShpkPath))) - tab.LoadShpk(moddedPath); - - if (!gamePath.Path.Equals(moddedPath.InternalName)) - { - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, - gamePath.Path.Equals(tab.LoadedShpkPath.InternalName))) - tab.LoadShpk(new FullPath(gamePath)); - } - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - } - - private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) - { - if (tab.ShaderKeys.Count == 0) - return false; - - var ret = false; - foreach (var (label, index, description, monoFont, values) in tab.ShaderKeys) - { - using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); - ref var key = ref tab.Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); - var currentValue = key.Value; - var (currentLabel, _, currentDescription) = - values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); - if (!disabled && shpkKey.HasValue) - { - ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) - { - if (c) - foreach (var (valueLabel, value, valueDescription) in values) - { - if (ImGui.Selectable(valueLabel, value == currentValue)) - { - key.Value = value; - ret = true; - tab.Update(); - } - - if (valueDescription.Length > 0) - ImGuiUtil.SelectableHelpMarker(valueDescription); - } - } - - ImGui.SameLine(); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - else if (description.Length > 0 || currentDescription.Length > 0) - { - ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", - description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); - } - else - { - ImGui.TextUnformatted($"{label}: {currentLabel}"); - } - } - - return ret; - } - - private static void DrawMaterialShaders(MtrlTab tab) - { - if (tab.AssociatedShpk == null) - return; - - ImRaii.TreeNode(tab.VertexShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - ImRaii.TreeNode(tab.PixelShadersString, ImGuiTreeNodeFlags.Leaf).Dispose(); - - if (tab.ShaderComment.Length > 0) - { - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGui.TextUnformatted(tab.ShaderComment); - } - } - - private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) - { - if (tab.Constants.Count == 0) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Material Constants")) - return false; - - using var _ = ImRaii.PushId("MaterialConstants"); - - var ret = false; - foreach (var (header, group) in tab.Constants) - { - using var t = ImRaii.TreeNode(header, ImGuiTreeNodeFlags.DefaultOpen); - if (!t) - continue; - - foreach (var (label, constantIndex, slice, description, monoFont, editor) in group) - { - var constant = tab.Mtrl.ShaderPackage.Constants[constantIndex]; - var buffer = tab.Mtrl.GetConstantValues(constant); - if (buffer.Length > 0) - { - using var id = ImRaii.PushId($"##{constant.Id:X8}:{slice.Start}"); - ImGui.SetNextItemWidth(250.0f); - if (editor.Draw(buffer[slice], disabled)) - { - ret = true; - tab.SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); - } - - ImGui.SameLine(); - using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - } - } - - return ret; - } - - private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, int textureIdx, int samplerIdx) - { - var ret = false; - ref var texture = ref tab.Mtrl.Textures[textureIdx]; - ref var sampler = ref tab.Mtrl.ShaderPackage.Samplers[samplerIdx]; - - // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) - { - fixed (ushort* v2 = &v) - { - return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, nint.Zero, nint.Zero, "%04X", flags); - } - } - - static bool ComboTextureAddressMode(string label, ref uint samplerFlags, int bitOffset) - { - var current = (TextureAddressMode)((samplerFlags >> bitOffset) & 0x3u); - using var c = ImRaii.Combo(label, current.ToString()); - if (!c) - return false; - - var ret = false; - foreach (var value in Enum.GetValues()) - { - if (ImGui.Selectable(value.ToString(), value == current)) - { - samplerFlags = (samplerFlags & ~(0x3u << bitOffset)) | ((uint)value << bitOffset); - ret = true; - } - - ImGuiUtil.SelectableHelpMarker(TextureAddressModeTooltips[(int)value]); - } - - return ret; - } - - var dx11 = texture.DX11; - if (ImGui.Checkbox("Prepend -- to the file name on DirectX 11", ref dx11)) - { - texture.DX11 = dx11; - ret = true; - } - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ComboTextureAddressMode("##UAddressMode", ref sampler.Flags, 2)) - { - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("U Address Mode", "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ComboTextureAddressMode("##VAddressMode", ref sampler.Flags, 0)) - { - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("V Address Mode", "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); - - var lodBias = ((int)(sampler.Flags << 12) >> 22) / 64.0f; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.DragFloat("##LoDBias", ref lodBias, 0.1f, -8.0f, 7.984375f)) - { - sampler.Flags = (uint)((sampler.Flags & ~0x000FFC00) - | ((uint)((int)Math.Round(Math.Clamp(lodBias, -8.0f, 7.984375f) * 64.0f) & 0x3FF) << 10)); - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Level of Detail Bias", - "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); - - var minLod = (int)((sampler.Flags >> 20) & 0xF); - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.DragInt("##MinLoD", ref minLod, 0.1f, 0, 15)) - { - sampler.Flags = (uint)((sampler.Flags & ~0x00F00000) | ((uint)Math.Clamp(minLod, 0, 15) << 20)); - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Minimum Level of Detail", - "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); - - using var t = ImRaii.TreeNode("Advanced Settings"); - if (!t) - return ret; - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (InputHexUInt16("Texture Flags", ref texture.Flags, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) - ret = true; - - var samplerFlags = (int)sampler.Flags; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) - { - sampler.Flags = (uint)samplerFlags; - ret = true; - tab.SetSamplerFlags(sampler.SamplerId, (uint)samplerFlags); - } - - return ret; - } - - private bool DrawMaterialShader(MtrlTab tab, bool disabled) - { - var ret = false; - if (ImGui.CollapsingHeader(tab.ShaderHeader)) - { - ret |= DrawPackageNameInput(tab, disabled); - ret |= DrawShaderFlagsInput(tab, disabled); - DrawCustomAssociations(tab); - ret |= DrawMaterialShaderKeys(tab, disabled); - DrawMaterialShaders(tab); - } - - if (tab.AssociatedShpkDevkit == null) - { - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - GC.KeepAlive(tab); - - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = - (textColor & 0xFF000000u) - | ((textColor & 0x00FEFEFE) >> 1) - | (tab.AssociatedShpk == null ? 0x80u : 0x8080u); // Half red or yellow - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - - ImGui.TextUnformatted(tab.AssociatedShpk == null - ? "Unable to find a suitable .shpk file for cross-references. Some functionality will be missing." - : "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."); - } - - return ret; - } - - private static string? MaterialParamName(bool componentOnly, int offset) - { - if (offset < 0) - return null; - - return (componentOnly, offset & 0x3) switch - { - (true, 0) => "x", - (true, 1) => "y", - (true, 2) => "z", - (true, 3) => "w", - (false, 0) => $"[{offset >> 2:D2}].x", - (false, 1) => $"[{offset >> 2:D2}].y", - (false, 2) => $"[{offset >> 2:D2}].z", - (false, 3) => $"[{offset >> 2:D2}].w", - _ => null, - }; - } - - private static string VectorSwizzle(int firstComponent, int lastComponent) - => (firstComponent, lastComponent) switch - { - (0, 4) => " ", - (0, 0) => ".x ", - (0, 1) => ".xy ", - (0, 2) => ".xyz ", - (0, 3) => " ", - (1, 1) => ".y ", - (1, 2) => ".yz ", - (1, 3) => ".yzw ", - (2, 2) => ".z ", - (2, 3) => ".zw ", - (3, 3) => ".w ", - _ => string.Empty, - }; - - private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) - { - if (valueLength == 0 || valueOffset < 0) - return (null, false); - - var firstVector = valueOffset >> 2; - var lastVector = (valueOffset + valueLength - 1) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = (valueOffset + valueLength - 1) & 0x3; - if (firstVector == lastVector) - return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); - - var sb = new StringBuilder(128); - sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); - for (var i = firstVector + 1; i < lastVector; ++i) - sb.Append($", [{i}]"); - - sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); - return (sb.ToString(), false); - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 5a8fb13a..ee883daf 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,11 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Text; -using OtterGui.Widgets; -using Penumbra.GameData.Files; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -17,177 +13,10 @@ public partial class ModEditWindow private bool DrawMaterialPanel(MtrlTab tab, bool disabled) { - DrawVersionUpdate(tab, disabled); - DrawMaterialLivePreviewRebind(tab, disabled); + if (tab.DrawVersionUpdate(disabled)) + _materialTab.SaveFile(); - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - var ret = DrawBackFaceAndTransparency(tab, disabled); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ret |= DrawMaterialShader(tab, disabled); - - ret |= DrawMaterialTextureChange(tab, disabled); - ret |= DrawMaterialColorTableChange(tab, disabled); - ret |= DrawMaterialConstants(tab, disabled); - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - DrawOtherMaterialDetails(tab.Mtrl, disabled); - - return !disabled && ret; - } - - private void DrawVersionUpdate(MtrlTab tab, bool disabled) - { - if (disabled || tab.Mtrl.IsDawnTrail) - return; - - if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, - "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, - new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) - return; - - tab.Mtrl.MigrateToDawntrail(); - _materialTab.SaveFile(); - } - - private static void DrawMaterialLivePreviewRebind(MtrlTab tab, bool disabled) - { - if (disabled) - return; - - if (ImGui.Button("Reload live preview")) - tab.BindToMaterialInstances(); - - if (tab.MaterialPreviewers.Count != 0 || tab.ColorTablePreviewers.Count != 0) - return; - - ImGui.SameLine(); - using var c = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - ImGui.TextUnformatted( - "The current material has not been found on your character. Please check the Import from Screen tab for more information."); - } - - private static bool DrawMaterialTextureChange(MtrlTab tab, bool disabled) - { - if (tab.Textures.Count == 0) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Textures and Samplers", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - var frameHeight = ImGui.GetFrameHeight(); - var ret = false; - using var table = ImRaii.Table("##Textures", 3); - - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, frameHeight); - ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale); - foreach (var (label, textureI, samplerI, description, monoFont) in tab.Textures) - { - using var _ = ImRaii.PushId(samplerI); - var tmp = tab.Mtrl.Textures[textureI].Path; - var unfolded = tab.UnfoldedTextures.Contains(samplerI); - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton((unfolded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight).ToIconString(), - new Vector2(frameHeight), - "Settings for this texture and the associated sampler", false, true)) - { - unfolded = !unfolded; - if (unfolded) - tab.UnfoldedTextures.Add(samplerI); - else - tab.UnfoldedTextures.Remove(samplerI); - } - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None) - && tmp.Length > 0 - && tmp != tab.Mtrl.Textures[textureI].Path) - { - ret = true; - tab.Mtrl.Textures[textureI].Path = tmp; - } - - ImGui.TableNextColumn(); - using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) - { - ImGui.AlignTextToFramePadding(); - if (description.Length > 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); - } - - if (unfolded) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ret |= DrawMaterialSampler(tab, disabled, textureI, samplerI); - ImGui.TableNextColumn(); - } - } - - return ret; - } - - private static bool DrawBackFaceAndTransparency(MtrlTab tab, bool disabled) - { - const uint transparencyBit = 0x10; - const uint backfaceBit = 0x01; - - var ret = false; - - using var dis = ImRaii.Disabled(disabled); - - var tmp = (tab.Mtrl.ShaderPackage.Flags & transparencyBit) != 0; - if (ImGui.Checkbox("Enable Transparency", ref tmp)) - { - tab.Mtrl.ShaderPackage.Flags = - tmp ? tab.Mtrl.ShaderPackage.Flags | transparencyBit : tab.Mtrl.ShaderPackage.Flags & ~transparencyBit; - ret = true; - tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); - } - - ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); - tmp = (tab.Mtrl.ShaderPackage.Flags & backfaceBit) != 0; - if (ImGui.Checkbox("Hide Backfaces", ref tmp)) - { - tab.Mtrl.ShaderPackage.Flags = tmp ? tab.Mtrl.ShaderPackage.Flags | backfaceBit : tab.Mtrl.ShaderPackage.Flags & ~backfaceBit; - ret = true; - tab.SetShaderPackageFlags(tab.Mtrl.ShaderPackage.Flags); - } - - return ret; - } - - private static void DrawOtherMaterialDetails(MtrlFile file, bool _) - { - if (!ImGui.CollapsingHeader("Further Content")) - return; - - using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) - { - if (sets) - foreach (var set in file.UvSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) - { - if (sets) - foreach (var set in file.ColorSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); - } - - if (file.AdditionalData.Length <= 0) - return; - - using var t = ImRaii.TreeNode($"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData"); - if (t) - Widget.DrawHexViewer(file.AdditionalData); + return tab.DrawPanel(disabled); } private void DrawMaterialReassignmentTab() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 55b7e748..6fb223df 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -15,6 +15,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { + private readonly FileDialogService _fileDialog; private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; private readonly Dictionary _quickImportWritables = new(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 0d3dce8c..f28cb632 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -13,10 +13,8 @@ using Penumbra.Collections.Manager; using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; -using Penumbra.GameData.Interop; using Penumbra.Import.Models; using Penumbra.Import.Textures; -using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.ResourceTree; using Penumbra.Meta; using Penumbra.Mods; @@ -26,6 +24,7 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; +using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; using Penumbra.Util; @@ -39,20 +38,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public readonly MigrationManager MigrationManager; - private readonly PerformanceTracker _performance; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly ItemSwapTab _itemSwapTab; - private readonly MetaFileManager _metaFileManager; - private readonly ActiveCollections _activeCollections; - private readonly StainService _stainService; - private readonly ModMergeTab _modMergeTab; - private readonly CommunicatorService _communicator; - private readonly IDragDropManager _dragDropManager; - private readonly IDataManager _gameData; - private readonly IFramework _framework; - private readonly ObjectManager _objects; - private readonly CharacterBaseDestructor _characterBaseDestructor; + private readonly PerformanceTracker _performance; + private readonly ModEditor _editor; + private readonly Configuration _config; + private readonly ItemSwapTab _itemSwapTab; + private readonly MetaFileManager _metaFileManager; + private readonly ActiveCollections _activeCollections; + private readonly ModMergeTab _modMergeTab; + private readonly CommunicatorService _communicator; + private readonly IDragDropManager _dragDropManager; + private readonly IDataManager _gameData; + private readonly IFramework _framework; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; @@ -541,7 +537,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService /// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them. /// If no redirection is found in either of those options, returns the original path. /// - private FullPath FindBestMatch(Utf8GamePath path) + internal FullPath FindBestMatch(Utf8GamePath path) { var currentFile = _activeCollections.Current.ResolvePath(path); if (currentFile != null) @@ -562,7 +558,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return new FullPath(path); } - private HashSet FindPathsStartingWith(CiByteString prefix) + internal HashSet FindPathsStartingWith(CiByteString prefix) { var ret = new HashSet(); @@ -587,34 +583,32 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab, + ActiveCollections activeCollections, ModMergeTab modMergeTab, CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, - ResourceTreeViewerFactory resourceTreeViewerFactory, ObjectManager objects, IFramework framework, - CharacterBaseDestructor characterBaseDestructor, MetaDrawers metaDrawers, MigrationManager migrationManager) + ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework, + MetaDrawers metaDrawers, MigrationManager migrationManager, + MtrlTabFactory mtrlTabFactory) : base(WindowBaseLabel) { - _performance = performance; - _itemSwapTab = itemSwapTab; - _gameData = gameData; - _config = config; - _editor = editor; - _metaFileManager = metaFileManager; - _stainService = stainService; - _activeCollections = activeCollections; - _modMergeTab = modMergeTab; - _communicator = communicator; - _dragDropManager = dragDropManager; - _textures = textures; - _models = models; - _fileDialog = fileDialog; - _objects = objects; - _framework = framework; - _characterBaseDestructor = characterBaseDestructor; - MigrationManager = migrationManager; - _metaDrawers = metaDrawers; + _performance = performance; + _itemSwapTab = itemSwapTab; + _gameData = gameData; + _config = config; + _editor = editor; + _metaFileManager = metaFileManager; + _activeCollections = activeCollections; + _modMergeTab = modMergeTab; + _communicator = communicator; + _dragDropManager = dragDropManager; + _textures = textures; + _models = models; + _fileDialog = fileDialog; + _framework = framework; + MigrationManager = migrationManager; + _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, - (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); + (bytes, path, writable) => mtrlTabFactory.Create(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path)); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 4d0c62af..d135e10c 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -24,6 +24,7 @@ public enum ColorId NoAssignment, SelectorPriority, InGameHighlight, + InGameHighlight2, ResTreeLocalPlayer, ResTreePlayer, ResTreeNetworked, @@ -70,7 +71,8 @@ public static class Colors ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), - ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ab47ce7c..41ca8d6e 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -327,6 +327,9 @@ public class SettingsTab : ITab, IUiService UiHelpers.DefaultLineSpace(); DrawModHandlingSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModEditorSettings(); ImGui.NewLine(); } @@ -723,6 +726,15 @@ public class SettingsTab : ITab, IUiService "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root."); } + + /// Draw all settings pertaining to advanced editing of mods. + private void DrawModEditorSettings() + { + Checkbox("Advanced Editing: Edit Raw Tile UV Transforms", + "Edit the raw matrix components of tile UV transforms, instead of having them decomposed into scale, rotation and shear.", + _config.EditRawTileTransforms, v => _config.EditRawTileTransforms = v); + } + #endregion /// Draw the entire Color subsection. From f4fe3605f003456a2ba7d1310c907b2d54d9547b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:42:32 +0200 Subject: [PATCH 313/865] DT material editor, new color tables --- .../Materials/MtrlTab.ColorTable.cs | 562 ++++++++++++++++++ .../Materials/MtrlTab.CommonColorTable.cs | 1 + 2 files changed, 563 insertions(+) create mode 100644 Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs new file mode 100644 index 00000000..dc87ec41 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -0,0 +1,562 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Services; + +namespace Penumbra.UI.AdvancedWindow.Materials; + +public partial class MtrlTab +{ + private const float ColorTableScalarSize = 65.0f; + + private int _colorTableSelectedPair = 0; + + private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + DrawColorTablePairSelector(table, disabled); + return DrawColorTablePairEditor(table, dyeTable, disabled); + } + + private void DrawColorTablePairSelector(ColorTable table, bool disabled) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + var style = ImGui.GetStyle(); + var itemSpacing = style.ItemSpacing.X; + var itemInnerSpacing = style.ItemInnerSpacing.X; + var framePadding = style.FramePadding; + var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; + var frameHeight = ImGui.GetFrameHeight(); + var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) + { + for (var j = 0; j < 8; ++j) + { + var pairIndex = i + j; + using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) + { + if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) + _colorTableSelectedPair = pairIndex; + } + + var rcMin = ImGui.GetItemRectMin() + framePadding; + var rcMax = ImGui.GetItemRectMax() - framePadding; + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 }, + rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 }, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)) + ); + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing }, + rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing }, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)) + ); + CtBlendRect( + rcMin with { X = rcMax.X - frameHeight }, rcMax, + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)), + ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)) + ); + if (j < 7) + ImGui.SameLine(); + + var cursor = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 0.5f) - highlighterSize.Y * 0.5f }); + font.Pop(); + ColorTableHighlightButton(pairIndex, disabled); + font.Push(UiBuilder.MonoFont); + ImGui.SetCursorScreenPos(cursor); + } + } + } + + private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled) + { + var retA = false; + var retB = false; + ref var rowA = ref table[_colorTableSelectedPair << 1]; + ref var rowB = ref table[(_colorTableSelectedPair << 1) | 1]; + var dyeA = dyeTable != null ? dyeTable[_colorTableSelectedPair << 1] : default; + var dyeB = dyeTable != null ? dyeTable[(_colorTableSelectedPair << 1) | 1] : default; + var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; + var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; + var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); + var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); + ImUtf8.SameLineInner(); + retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + ImGui.SameLine(); + CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); + columns.Next(); + ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); + ImUtf8.SameLineInner(); + retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + ImGui.SameLine(); + CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); + } + + DrawHeader(" Colors"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("ColorsA"u8)) + retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("ColorsB"u8)) + retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Physical Parameters"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("PbrA"u8)) + retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("PbrB"u8)) + retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Sheen Layer Parameters"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("SheenA"u8)) + retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("SheenB"u8)) + retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Pair Blending"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("BlendingA"u8)) + retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("BlendingB"u8)) + retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + DrawHeader(" Material Template"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("TemplateA"u8)) + retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("TemplateB"u8)) + retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + if (dyeTable != null) + { + DrawHeader(" Dye Properties"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("DyeA"u8)) + retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("DyeB"u8)) + retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + } + + DrawHeader(" Further Content"u8); + using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + { + using var dis = ImRaii.Disabled(disabled); + using (var id = ImUtf8.PushId("FurtherA"u8)) + retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + columns.Next(); + using (var id = ImUtf8.PushId("FurtherB"u8)) + retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } + + if (retA) + UpdateColorTableRowPreview(_colorTableSelectedPair << 1); + if (retB) + UpdateColorTableRowPreview((_colorTableSelectedPair << 1) | 1); + + return retA | retB; + } + + /// Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. + private static void DrawHeader(ReadOnlySpan label) + { + var headerColor = ImGui.GetColorU32(ImGuiCol.Header); + using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); + ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); + } + + private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() * 2.0f; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor, + c => table[rowIdx].DiffuseColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeDiffuseColor"u8, "Apply Diffuse Color on Dye"u8, dye.DiffuseColor, + b => dyeTable[rowIdx].DiffuseColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewDiffuseColor"u8, "Dye Preview for Diffuse Color"u8, dyePack?.DiffuseColor); + } + + ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor, + c => table[rowIdx].SpecularColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, + b => dyeTable[rowIdx].SpecularColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewSpecularColor"u8, "Dye Preview for Specular Color"u8, dyePack?.SpecularColor); + } + + ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor, + c => table[rowIdx].EmissiveColor = c); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, + b => dyeTable[rowIdx].EmissiveColor = b); + ImUtf8.SameLineInner(); + CtColorPicker("##dyePreviewEmissiveColor"u8, "Dye Preview for Emissive Color"u8, dyePack?.EmissiveColor); + } + + return ret; + } + + private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var isRowB = (rowIdx & 1) != 0; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, + v => table[rowIdx].Anisotropy = v); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, dye.Anisotropy, + b => dyeTable[rowIdx].Anisotropy = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, dyePack?.Anisotropy, "%.2f"u8); + } + + return ret; + } + + private bool DrawTemplate(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var itemSpacing = ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f; + var subcolWidth = CalculateSubcolumnWidth(2); + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, + v => table[rowIdx].ShaderId = v); + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); + ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false, + v => table[rowIdx].SphereMapIndex = v); + ImUtf8.SameLineInner(); + ImUtf8.Text("Sphere Map"u8); + if (dyeTable != null) + { + var textRectMin = ImGui.GetItemRectMin(); + var textRectMax = ImGui.GetItemRectMax(); + ImGui.SameLine(dyeOffset); + var cursor = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - ImGui.GetFrameHeight() * 0.5f }); + ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex, + b => dyeTable[rowIdx].SphereMapIndex = b); + ImUtf8.SameLineInner(); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); + ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); + using var dis = ImRaii.Disabled(); + CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); + } + + ImGui.Dummy(new Vector2(64.0f, 0.0f)); + ImGui.SameLine(); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask, + b => dyeTable[rowIdx].SphereMapMask = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; + var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y; + var lineHeight = Math.Max(leftLineHeight, rightLineHeight); + var cursorPos = ImGui.GetCursorScreenPos(); + ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); + ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); + ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, + v => table[rowIdx].TileIndex = v); + ImUtf8.SameLineInner(); + ImUtf8.Text("Tile"u8); + + ImGui.SameLine(subcolWidth); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f, }); + using (var cld = ImUtf8.Child("###TileProperties"u8, new(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)), false)) + { + ImGui.Dummy(new Vector2(scalarSize, 0.0f)); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].TileAlpha = (Half)(v * 0.01f)); + + ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, + m => table[rowIdx].TileTransform = m); + ImUtf8.SameLineInner(); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); + ImUtf8.Text("Tile Transform"u8); + } + + return ret; + } + + private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Roughness = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness, + b => dyeTable[rowIdx].Roughness = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8); + } + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].Metalness = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(subcolWidth + dyeOffset); + ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, + b => dyeTable[rowIdx].Metalness = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewMetalness"u8, "Dye Preview for Metalness"u8, (float?)dyePack?.Metalness * 100.0f, "%.0f%%"u8); + } + + return ret; + } + + private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SheenRate = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate, + b => dyeTable[rowIdx].SheenRate = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8); + } + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); + if (dyeTable != null) + { + ImGui.SameLine(subcolWidth + dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, + b => dyeTable[rowIdx].SheenTintRate = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenTintRate"u8, "Dye Preview for Sheen Tint"u8, (float?)dyePack?.SheenTintRate * 100.0f, "%.0f%%"u8); + } + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, + v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture, + b => dyeTable[rowIdx].SheenAperture = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, "%.0f%%"u8); + } + + return ret; + } + + private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + + var ret = false; + ref var row = ref table[rowIdx]; + var dye = dyeTable != null ? dyeTable[rowIdx] : default; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar11 = v); + if (dyeTable != null) + { + ImGui.SameLine(dyeOffset); + ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3, + b => dyeTable[rowIdx].Scalar3 = b); + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(scalarSize); + CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8); + } + + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar3 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar7 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar15 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar17 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar20 = v); + + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar22 = v); + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragHalf("Field #23"u8, default, row.Scalar23, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, + v => table[rowIdx].Scalar23 = v); + + return ret; + } + + private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx) + { + var scalarSize = ColorTableScalarSize * UiHelpers.Scale; + var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f; + var subcolWidth = CalculateSubcolumnWidth(2, applyButtonWidth); + + var ret = false; + ref var dye = ref dyeTable[rowIdx]; + + ImGui.SetNextItemWidth(scalarSize); + ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f, + value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); + ImGui.SameLine(subcolWidth); + ImGui.SetNextItemWidth(scalarSize); + if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, + scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + { + dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; + ret = true; + } + ImUtf8.SameLineInner(); + ImUtf8.Text("Dye Template"u8); + ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X); + using var dis = ImRaii.Disabled(!dyePack.HasValue); + if (ImUtf8.Button("Apply Preview Dye"u8)) + { + ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); + } + + return ret; + } + + private static void CenteredTextInRest(string text) + => AlignedTextInRest(text, 0.5f); + + private static void AlignedTextInRest(string text, float alignment) + { + var width = ImGui.CalcTextSize(text).X; + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((ImGui.GetContentRegionAvail().X - width) * alignment, 0.0f)); + ImGui.TextUnformatted(text); + } + + private static float CalculateSubcolumnWidth(int numSubcolumns, float reservedSpace = 0.0f) + { + var itemSpacing = ImGui.GetStyle().ItemSpacing.X; + return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubcolumns - 1)) / numSubcolumns + itemSpacing; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 937614de..2b093e23 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -52,6 +52,7 @@ public partial class MtrlTab { LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), _ => false, }; From 5323add662874d3b0286f8ed35930b620fd99630 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:45:52 +0200 Subject: [PATCH 314/865] Improve ShPk tab --- .../ModEditWindow.ShaderPackages.cs | 287 ++++++++++++++---- .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 287 ++++++++++++++++-- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 3 files changed, 494 insertions(+), 82 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 017478a7..8a1c729c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -1,7 +1,6 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using ImGuiNET; -using Lumina.Misc; using OtterGui.Raii; using OtterGui; using OtterGui.Classes; @@ -11,6 +10,9 @@ using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; +using Penumbra.GameData.Files.ShaderStructs; +using OtterGui.Text; +using Penumbra.GameData.Structs; namespace Penumbra.UI.AdvancedWindow; @@ -24,6 +26,9 @@ public partial class ModEditWindow { DrawShaderPackageSummary(file); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); + DrawShaderPackageFilterSection(file); + var ret = false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); ret |= DrawShaderPackageShaderArray(file, "Vertex Shader", file.Shpk.VertexShaders, disabled); @@ -50,15 +55,16 @@ public partial class ModEditWindow private static void DrawShaderPackageSummary(ShpkTab tab) { + if (tab.Shpk.IsLegacy) + { + ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.", + ImGuiUtil.HalfBlendText(0x80u)); // Half red + } ImGui.TextUnformatted(tab.Header); if (!tab.Shpk.Disassembled) { - var textColor = ImGui.GetColorU32(ImGuiCol.Text); - var textColorWarning = (textColor & 0xFF000000u) | ((textColor & 0x00FEFEFE) >> 1) | 0x80u; // Half red - - using var c = ImRaii.PushColor(ImGuiCol.Text, textColorWarning); - - ImGui.TextUnformatted("Your system doesn't support disassembling shaders. Some functionality will be missing."); + ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.", + ImGuiUtil.HalfBlendText(0x80u)); // Half red } } @@ -123,6 +129,7 @@ public partial class ModEditWindow { shaders[idx].UpdateResources(tab.Shpk); tab.Shpk.UpdateResources(); + tab.UpdateFilteredUsed(); } catch (Exception e) { @@ -149,6 +156,97 @@ public partial class ModEditWindow ImGuiInputTextFlags.ReadOnly, null, null); } + private static void DrawShaderUsage(ShpkTab tab, Shader shader) + { + using (var node = ImUtf8.TreeNode("Used with Shader Keys"u8)) + { + if (node) + { + foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex()) + { + ImRaii.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + } + } + + ImRaii.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + + private static void DrawShaderPackageFilterSection(ShpkTab tab) + { + if (!ImUtf8.CollapsingHeader(tab.FilterPopCount == tab.FilterMaximumPopCount ? "Filters###Filters"u8 : "Filters (ACTIVE)###Filters"u8)) + return; + + foreach (var (key, keyIdx) in tab.Shpk.SystemKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"System Key {tab.TryResolveName(key.Id)}", ref tab.FilterSystemValues[keyIdx]); + + foreach (var (key, keyIdx) in tab.Shpk.SceneKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Scene Key {tab.TryResolveName(key.Id)}", ref tab.FilterSceneValues[keyIdx]); + + foreach (var (key, keyIdx) in tab.Shpk.MaterialKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Material Key {tab.TryResolveName(key.Id)}", ref tab.FilterMaterialValues[keyIdx]); + + foreach (var (_, keyIdx) in tab.Shpk.SubViewKeys.WithIndex()) + DrawShaderPackageFilterSet(tab, $"Sub-View Key #{keyIdx}", ref tab.FilterSubViewValues[keyIdx]); + + DrawShaderPackageFilterSet(tab, "Passes", ref tab.FilterPasses); + } + + private static void DrawShaderPackageFilterSet(ShpkTab tab, string label, ref SharedSet values) + { + if (values.PossibleValues == null) + { + ImRaii.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + return; + } + + using var node = ImRaii.TreeNode(label); + if (!node) + return; + + foreach (var value in values.PossibleValues) + { + var contains = values.Contains(value); + if (!ImGui.Checkbox($"{tab.TryResolveName(value)}", ref contains)) + continue; + if (contains) + { + if (values.AddExisting(value)) + { + ++tab.FilterPopCount; + tab.UpdateFilteredUsed(); + } + } + else + { + if (values.Remove(value)) + { + --tab.FilterPopCount; + tab.UpdateFilteredUsed(); + } + } + } + } + private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) @@ -157,8 +255,11 @@ public partial class ModEditWindow var ret = false; for (var idx = 0; idx < shaders.Length; ++idx) { - var shader = shaders[idx]; - using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + var shader = shaders[idx]; + if (!tab.IsFilterMatch(shader)) + continue; + + using var t = ImRaii.TreeNode($"{objectName} #{idx}"); if (!t) continue; @@ -169,9 +270,11 @@ public partial class ModEditWindow DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, true); - ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, true); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, true); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); + if (!tab.Shpk.IsLegacy) + ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); if (shader.DeclaredInputs != 0) ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); @@ -187,12 +290,14 @@ public partial class ModEditWindow if (tab.Shpk.Disassembled) DrawRawDisassembly(shader); + + DrawShaderUsage(tab, shader); } return ret; } - private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool disabled) + private static bool DrawShaderPackageResource(string slotLabel, bool withSize, ref Resource resource, bool hasFilter, bool disabled) { var ret = false; if (!disabled) @@ -205,16 +310,26 @@ public partial class ModEditWindow if (resource.Used == null) return ret; - var usedString = UsedComponentString(withSize, resource); + var usedString = UsedComponentString(withSize, false, resource); if (usedString.Length > 0) - ImRaii.TreeNode($"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImRaii.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + if (hasFilter) + { + var filteredUsedString = UsedComponentString(withSize, true, resource); + if (filteredUsedString.Length > 0) + ImRaii.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + else + ImRaii.TreeNode("Unused within Filters", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } + } else - ImRaii.TreeNode("Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode(hasFilter ? "Globally Unused" : "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return ret; } - private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, bool disabled) { if (resources.Length == 0) return false; @@ -233,7 +348,7 @@ public partial class ModEditWindow using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); font.Dispose(); if (t2) - ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, disabled); + ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled); } return ret; @@ -268,7 +383,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { ImGui.TextUnformatted(tab.Shpk.Disassembled - ? "Parameter positions (continuations are grayed out, unused values are red):" + ? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):" : "Parameter positions (continuations are grayed out):"); using var table = ImRaii.Table("##MaterialParamLayout", 5, @@ -276,17 +391,17 @@ public partial class ModEditWindow if (!table) return false; - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale); - ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); - ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); - ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); - ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 40 * UiHelpers.Scale); + ImGui.TableSetupColumn("x", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("y", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("z", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); + ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); ImGui.TableHeadersRow(); var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); - var textColorCont = (textColorStart & 0x00FFFFFFu) | ((textColorStart & 0xFE000000u) >> 1); // Half opacity - var textColorUnusedStart = (textColorStart & 0xFF000000u) | ((textColorStart & 0x00FEFEFE) >> 1) | 0x80u; // Half red - var textColorUnusedCont = (textColorUnusedStart & 0x00FFFFFFu) | ((textColorUnusedStart & 0xFE000000u) >> 1); + var textColorCont = ImGuiUtil.HalfTransparent(textColorStart); // Half opacity + var textColorUnusedStart = ImGuiUtil.HalfBlend(textColorStart, 0x80u); // Half red + var textColorUnusedCont = ImGuiUtil.HalfTransparent(textColorUnusedStart); var ret = false; for (var i = 0; i < tab.Matrix.GetLength(0); ++i) @@ -296,14 +411,13 @@ public partial class ModEditWindow for (var j = 0; j < 4; ++j) { var (name, tooltip, idx, colorType) = tab.Matrix[i, j]; - var color = colorType switch - { - ShpkTab.ColorType.Unused => textColorUnusedStart, - ShpkTab.ColorType.Used => textColorStart, - ShpkTab.ColorType.Continuation => textColorUnusedCont, - ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont, - _ => textColorStart, - }; + var color = textColorStart; + if (!colorType.HasFlag(ShpkTab.ColorType.Used)) + color = ImGuiUtil.HalfBlend(color, 0x80u); // Half red + else if (!colorType.HasFlag(ShpkTab.ColorType.FilteredUsed)) + color = ImGuiUtil.HalfBlend(color, 0x8080u); // Half yellow + if (colorType.HasFlag(ShpkTab.ColorType.Continuation)) + color = ImGuiUtil.HalfTransparent(color); // Half opacity using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) @@ -331,6 +445,35 @@ public partial class ModEditWindow return ret; } + private static void DrawShaderPackageMaterialDevkitExport(ShpkTab tab) + { + if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8)) + return; + + tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", ".json", DoSave, null, false); + + void DoSave(bool success, string path) + { + if (!success) + return; + + try + { + File.WriteAllText(path, tab.ExportDevkit().ToString()); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, $"Could not export dev-kit for {Path.GetFileName(tab.FilePath)} to {path}.", + NotificationType.Error, false); + return; + } + + Penumbra.Messager.NotificationMessage( + $"Material dev-kit file for {Path.GetFileName(tab.FilePath)} exported successfully to {Path.GetFileName(path)}.", + NotificationType.Success, false); + } + } + private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); @@ -396,23 +539,25 @@ public partial class ModEditWindow DrawShaderPackageEndCombo(tab); ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - if (ImGui.InputText("Name", ref tab.NewMaterialParamName, 63)) - tab.NewMaterialParamId = Crc32.Get(tab.NewMaterialParamName, 0xFFFFFFFFu); + var newName = tab.NewMaterialParamName.Value!; + if (ImGui.InputText("Name", ref newName, 63)) + tab.NewMaterialParamName = newName; - var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamId) + var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32) ? "The ID is already in use. Please choose a different name." : string.Empty; - if (!ImGuiUtil.DrawDisabledButton($"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), + if (!ImGuiUtil.DrawDisabledButton($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip, tooltip.Length > 0)) return false; tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam { - Id = tab.NewMaterialParamId, + Id = tab.NewMaterialParamName.Crc32, ByteOffset = (ushort)(tab.Orphans[tab.NewMaterialParamStart].Index << 2), ByteSize = (ushort)((tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1) << 2), }); + tab.AddNameToCache(tab.NewMaterialParamName); tab.Update(); return true; } @@ -434,6 +579,9 @@ public partial class ModEditWindow else if (!disabled && sizeWellDefined) ret |= DrawShaderPackageNewParameter(tab); + if (tab.Shpk.Disassembled) + DrawShaderPackageMaterialDevkitExport(tab); + return ret; } @@ -444,14 +592,17 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Shader Resources")) return false; - ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, disabled); - ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, disabled); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled); + var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount; + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); + if (!tab.Shpk.IsLegacy) + ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); return ret; } - private static void DrawKeyArray(string arrayName, bool withId, IReadOnlyCollection keys) + private static void DrawKeyArray(ShpkTab tab, string arrayName, bool withId, IReadOnlyCollection keys) { if (keys.Count == 0) return; @@ -463,12 +614,11 @@ public partial class ModEditWindow using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}"); + using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); if (t2) { - ImRaii.TreeNode($"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode($"Known Values: {string.Join(", ", Array.ConvertAll(key.Values, value => $"0x{value:X8}"))}", - ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImRaii.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } @@ -482,39 +632,46 @@ public partial class ModEditWindow if (!t) return; + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + foreach (var (node, idx) in tab.Shpk.Nodes.WithIndex()) { - using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + if (!tab.IsFilterMatch(node)) + continue; + + using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); if (!t2) continue; foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) { - ImRaii.TreeNode($"System Key 0x{tab.Shpk.SystemKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImRaii.TreeNode($"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) { - ImRaii.TreeNode($"Scene Key 0x{tab.Shpk.SceneKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImRaii.TreeNode($"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) { - ImRaii.TreeNode($"Material Key 0x{tab.Shpk.MaterialKeys[keyIdx].Id:X8} = 0x{key:X8}", + ImRaii.TreeNode($"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) - ImRaii.TreeNode($"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImRaii.TreeNode($"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode($"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImRaii.TreeNode($"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } @@ -526,10 +683,10 @@ public partial class ModEditWindow if (!ImGui.CollapsingHeader("Shader Selection")) return; - DrawKeyArray("System Keys", true, tab.Shpk.SystemKeys); - DrawKeyArray("Scene Keys", true, tab.Shpk.SceneKeys); - DrawKeyArray("Material Keys", true, tab.Shpk.MaterialKeys); - DrawKeyArray("Sub-View Keys", false, tab.Shpk.SubViewKeys); + DrawKeyArray(tab, "System Keys", true, tab.Shpk.SystemKeys); + DrawKeyArray(tab, "Scene Keys", true, tab.Shpk.SceneKeys); + DrawKeyArray(tab, "Material Keys", true, tab.Shpk.MaterialKeys); + DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys); DrawShaderPackageNodes(tab); using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); @@ -559,12 +716,14 @@ public partial class ModEditWindow } } - private static string UsedComponentString(bool withSize, in Resource resource) + private static string UsedComponentString(bool withSize, bool filtered, in Resource resource) { + var used = filtered ? resource.FilteredUsed : resource.Used; + var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically; var sb = new StringBuilder(256); if (withSize) { - foreach (var (components, i) in (resource.Used ?? Array.Empty()).WithIndex()) + foreach (var (components, i) in (used ?? Array.Empty()).WithIndex()) { switch (components) { @@ -582,7 +741,7 @@ public partial class ModEditWindow } } - switch (resource.UsedDynamically ?? 0) + switch (usedDynamically ?? 0) { case 0: break; case DisassembledShader.VectorComponents.All: @@ -590,7 +749,7 @@ public partial class ModEditWindow break; default: sb.Append("[*]."); - foreach (var c in resource.UsedDynamically!.Value.ToString().Where(char.IsUpper)) + foreach (var c in usedDynamically!.Value.ToString().Where(char.IsUpper)) sb.Append(char.ToLower(c)); sb.Append(", "); @@ -599,7 +758,7 @@ public partial class ModEditWindow } else { - var components = (resource.Used is { Length: > 0 } ? resource.Used[0] : 0) | (resource.UsedDynamically ?? 0); + var components = (used is { Length: > 0 } ? used[0] : 0) | (usedDynamically ?? 0); if ((components & DisassembledShader.VectorComponents.X) != 0) sb.Append("Red, "); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index 12b8d761..de20aa9f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -1,9 +1,12 @@ using Dalamud.Utility; -using Lumina.Misc; +using Newtonsoft.Json.Linq; using OtterGui; -using Penumbra.GameData.Data; +using OtterGui.Classes; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -12,18 +15,27 @@ public partial class ModEditWindow private class ShpkTab : IWritable { public readonly ShpkFile Shpk; + public readonly string FilePath; - public string NewMaterialParamName = string.Empty; - public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu); - public short NewMaterialParamStart; - public short NewMaterialParamEnd; + public Name NewMaterialParamName = string.Empty; + public short NewMaterialParamStart; + public short NewMaterialParamEnd; + + public SharedSet[] FilterSystemValues; + public SharedSet[] FilterSceneValues; + public SharedSet[] FilterMaterialValues; + public SharedSet[] FilterSubViewValues; + public SharedSet FilterPasses; + + public readonly int FilterMaximumPopCount; + public int FilterPopCount; public readonly FileDialogService FileDialog; public readonly string Header; public readonly string Extension; - public ShpkTab(FileDialogService fileDialog, byte[] bytes) + public ShpkTab(FileDialogService fileDialog, byte[] bytes, string filePath) { FileDialog = fileDialog; try @@ -34,6 +46,7 @@ public partial class ModEditWindow { Shpk = new ShpkFile(bytes, false); } + FilePath = filePath; Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch @@ -42,15 +55,36 @@ public partial class ModEditWindow ShpkFile.DxVersion.DirectX11 => ".dxbc", _ => throw new NotImplementedException(), }; + + FilterSystemValues = Array.ConvertAll(Shpk.SystemKeys, key => key.Values.FullSet()); + FilterSceneValues = Array.ConvertAll(Shpk.SceneKeys, key => key.Values.FullSet()); + FilterMaterialValues = Array.ConvertAll(Shpk.MaterialKeys, key => key.Values.FullSet()); + FilterSubViewValues = Array.ConvertAll(Shpk.SubViewKeys, key => key.Values.FullSet()); + FilterPasses = Shpk.Passes.FullSet(); + + FilterMaximumPopCount = FilterPasses.Count; + foreach (var key in Shpk.SystemKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.SceneKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.MaterialKeys) + FilterMaximumPopCount += key.Values.Count; + foreach (var key in Shpk.SubViewKeys) + FilterMaximumPopCount += key.Values.Count; + + FilterPopCount = FilterMaximumPopCount; + + UpdateNameCache(); + Shpk.UpdateFilteredUsed(IsFilterMatch); Update(); } [Flags] public enum ColorType : byte { - Unused = 0, Used = 1, - Continuation = 2, + FilteredUsed = 2, + Continuation = 4, } public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; @@ -58,10 +92,87 @@ public partial class ModEditWindow public readonly HashSet UsedIds = new(16); public readonly List<(string Name, short Index)> Orphans = new(16); + private readonly Dictionary _nameCache = []; + private readonly Dictionary, string> _nameSetCache = []; + private readonly Dictionary, string> _nameSetWithIdsCache = []; + + public void AddNameToCache(Name name) + { + if (name.Value != null) + _nameCache.TryAdd(name.Crc32, name); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + } + + public void UpdateNameCache() + { + static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) + { + foreach (var resource in resources) + nameCache.TryAdd(resource.Id, resource.Name); + } + + static void CollectKeyNames(Dictionary nameCache, ShpkFile.Key[] keys) + { + foreach (var key in keys) + { + var keyName = nameCache.TryResolve(Names.KnownNames, key.Id); + var valueNames = keyName.WithKnownSuffixes(); + foreach (var value in key.Values) + { + var valueName = valueNames.TryResolve(value); + if (valueName.Value != null) + nameCache.TryAdd(value, valueName); + } + } + } + + CollectResourceNames(_nameCache, Shpk.Constants); + CollectResourceNames(_nameCache, Shpk.Samplers); + CollectResourceNames(_nameCache, Shpk.Textures); + CollectResourceNames(_nameCache, Shpk.Uavs); + + CollectKeyNames(_nameCache, Shpk.SystemKeys); + CollectKeyNames(_nameCache, Shpk.SceneKeys); + CollectKeyNames(_nameCache, Shpk.MaterialKeys); + CollectKeyNames(_nameCache, Shpk.SubViewKeys); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Name TryResolveName(uint crc32) + => _nameCache.TryResolve(Names.KnownNames, crc32); + + public string NameSetToString(SharedSet nameSet, bool withIds = false) + { + var cache = withIds ? _nameSetWithIdsCache : _nameSetCache; + if (cache.TryGetValue(nameSet, out var nameSetStr)) + return nameSetStr; + if (withIds) + nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})")); + else + nameSetStr = string.Join(", ", nameSet.Select(TryResolveName)); + cache.Add(nameSet, nameSetStr); + return nameSetStr; + } + + public void UpdateFilteredUsed() + { + Shpk.UpdateFilteredUsed(IsFilterMatch); + + var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + UpdateColors(materialParams); + } + public void Update() { var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4; + var defaults = Shpk.MaterialParamsDefaults != null ? (ReadOnlySpan)Shpk.MaterialParamsDefaults : []; + var defaultFloats = MemoryMarshal.Cast(defaults); Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; MalformedParameters.Clear(); @@ -75,14 +186,14 @@ public partial class ModEditWindow var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); + MalformedParameters.Add($"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } if (iEnd >= numParameters) { MalformedParameters.Add( - $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})"); + $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"); continue; } @@ -91,9 +202,12 @@ public partial class ModEditWindow var end = i == iEnd ? jEnd : 3; for (var j = i == iStart ? jStart : 0; j <= end; ++j) { + var component = (i << 2) | j; var tt = - $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})"; - Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0); + $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"; + if (component < defaultFloats.Length) + tt += $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; + Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0); } } } @@ -151,7 +265,7 @@ public partial class ModEditWindow if (oldStart == linear) newMaterialParamStart = (short)Orphans.Count; - Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear)); + Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", linear)); } } @@ -168,11 +282,15 @@ public partial class ModEditWindow { var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All) | (materialParams?.UsedDynamically ?? 0); + var filteredUsedComponents = (materialParams?.FilteredUsed?[i] ?? DisassembledShader.VectorComponents.All) + | (materialParams?.FilteredUsedDynamically ?? 0); for (var j = 0; j < 4; ++j) { - var color = ((byte)usedComponents & (1 << j)) != 0 - ? ColorType.Used - : 0; + ColorType color = 0; + if (((byte)usedComponents & (1 << j)) != 0) + color |= ColorType.Used; + if (((byte)filteredUsedComponents & (1 << j)) != 0) + color |= ColorType.FilteredUsed; if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0) color |= ColorType.Continuation; @@ -182,6 +300,141 @@ public partial class ModEditWindow } } + public bool IsFilterMatch(ShpkFile.Shader shader) + { + if (!FilterPasses.Overlaps(shader.Passes)) + return false; + + for (var i = 0; i < shader.SystemValues!.Length; ++i) + { + if (!FilterSystemValues[i].Overlaps(shader.SystemValues[i])) + return false; + } + + for (var i = 0; i < shader.SceneValues!.Length; ++i) + { + if (!FilterSceneValues[i].Overlaps(shader.SceneValues[i])) + return false; + } + + for (var i = 0; i < shader.MaterialValues!.Length; ++i) + { + if (!FilterMaterialValues[i].Overlaps(shader.MaterialValues[i])) + return false; + } + + for (var i = 0; i < shader.SubViewValues!.Length; ++i) + { + if (!FilterSubViewValues[i].Overlaps(shader.SubViewValues[i])) + return false; + } + + return true; + } + + public bool IsFilterMatch(ShpkFile.Node node) + { + if (!node.Passes.Any(pass => FilterPasses.Contains(pass.Id))) + return false; + + for (var i = 0; i < node.SystemValues!.Length; ++i) + { + if (!FilterSystemValues[i].Overlaps(node.SystemValues[i])) + return false; + } + + for (var i = 0; i < node.SceneValues!.Length; ++i) + { + if (!FilterSceneValues[i].Overlaps(node.SceneValues[i])) + return false; + } + + for (var i = 0; i < node.MaterialValues!.Length; ++i) + { + if (!FilterMaterialValues[i].Overlaps(node.MaterialValues[i])) + return false; + } + + for (var i = 0; i < node.SubViewValues!.Length; ++i) + { + if (!FilterSubViewValues[i].Overlaps(node.SubViewValues[i])) + return false; + } + + return true; + } + + /// + /// Generates a minimal material dev-kit file for the given shader package. + /// + /// This file currently only hides globally unused material constants. + /// + public JObject ExportDevkit() + { + var devkit = new JObject(); + + var maybeMaterialParameter = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + if (maybeMaterialParameter.HasValue) + { + var materialParameter = maybeMaterialParameter.Value; + var materialParameterUsage = new IndexSet(materialParameter.Size << 2, true); + + var used = materialParameter.Used ?? []; + var usedDynamically = materialParameter.UsedDynamically ?? 0; + for (var i = 0; i < used.Length; ++i) + { + for (var j = 0; j < 4; ++j) + { + if (!(used[i] | usedDynamically).HasFlag((DisassembledShader.VectorComponents)(1 << j))) + materialParameterUsage[(i << 2) | j] = false; + } + } + + var dkConstants = new JObject(); + foreach (var param in Shpk.MaterialParams) + { + // Don't handle misaligned parameters. + if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) + continue; + + var start = param.ByteOffset >> 2; + var length = param.ByteSize >> 2; + + // If the parameter is fully used, don't include it. + if (!materialParameterUsage.Indices(start, length, true).Any()) + continue; + + var unusedSlices = new JArray(); + + if (materialParameterUsage.Indices(start, length).Any()) + { + foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true)) + { + unusedSlices.Add(new JObject + { + ["Type"] = "Hidden", + ["Offset"] = rgStart, + ["Length"] = rgEnd - rgStart, + }); + } + } + else + { + unusedSlices.Add(new JObject + { + ["Type"] = "Hidden", + }); + } + + dkConstants[param.Id.ToString()] = unusedSlices; + } + + devkit["Constants"] = dkConstants; + } + + return devkit; + } + public bool Valid => Shpk.Valid; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f28cb632..13458252 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -615,7 +615,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _shaderPackageTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => Mod?.ModPath.FullName ?? string.Empty, - (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); + (bytes, path, _) => new ShpkTab(_fileDialog, bytes, path)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; From c01aa000fb94afcbfc13fcfe87c4ab6bd5b5f86d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 17:46:29 +0200 Subject: [PATCH 315/865] Optimize I/O of ShPk for ResourceTree generation --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 11 ++++++++--- Penumbra/Interop/ResourceTree/TreeBuildCache.cs | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..41485d75 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -230,8 +230,8 @@ internal unsafe partial record ResolveContext( node.Children.Add(shpkNode); } - var shpkFile = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null; - var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; + var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) @@ -255,7 +255,12 @@ internal unsafe partial record ResolveContext( alreadyProcessedSamplerIds.Add(samplerId.Value); var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); if (samplerCrc.HasValue) - name = shpkFile?.GetSamplerById(samplerCrc.Value)?.Name ?? $"Texture 0x{samplerCrc.Value:X8}"; + { + if (shpkNames != null && shpkNames.TryGetValue(samplerCrc.Value, out var samplerName)) + name = samplerName.Value; + else + name = $"Texture 0x{samplerCrc.Value:X8}"; + } } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index ca5ff736..49e00547 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -1,8 +1,11 @@ +using System.IO.MemoryMappedFiles; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.String.Classes; @@ -11,7 +14,7 @@ namespace Penumbra.Interop.ResourceTree; internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors) { - private readonly Dictionary _shaderPackages = []; + private readonly Dictionary?> _shaderPackageNames = []; public unsafe bool IsLocalPlayerRelated(ICharacter character) { @@ -68,10 +71,10 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data } /// Try to read a shpk file from the given path and cache it on success. - public ShpkFile? ReadShaderPackage(FullPath path) - => ReadFile(dataManager, path, _shaderPackages, bytes => new ShpkFile(bytes)); + public IReadOnlyDictionary? ReadShaderPackageNames(FullPath path) + => ReadFile(dataManager, path, _shaderPackageNames, bytes => ShpkFile.FastExtractNames(bytes.Span)); - private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func parseFile) + private static T? ReadFile(IDataManager dataManager, FullPath path, Dictionary cache, Func, T> parseFile) where T : class { if (path.FullName.Length == 0) @@ -86,7 +89,8 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data { if (path.IsRooted) { - parsed = parseFile(File.ReadAllBytes(pathStr)); + using var mmFile = MmioMemoryManager.CreateFromFile(pathStr, access: MemoryMappedFileAccess.Read); + parsed = parseFile(mmFile.Memory); } else { From c849e310343b465d88619f05e9b30384d1caa709 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 19:48:42 +0200 Subject: [PATCH 316/865] RT: Use SpanTextWriter to assemble paths --- .../ResolveContext.PathResolution.cs | 28 +++++++++++++------ .../Interop/ResourceTree/ResolveContext.cs | 25 +++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 85b3284a..b99468f8 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using OtterGui.Text.HelperObjects; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -8,6 +9,7 @@ using Penumbra.Meta.Manipulations; using Penumbra.String; using Penumbra.String.Classes; using static Penumbra.Interop.Structs.StructExtensions; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -95,7 +97,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, Equipment.Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -125,7 +127,7 @@ internal partial record ResolveContext fileName.CopyTo(mirroredFileName); WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); @@ -144,7 +146,7 @@ internal partial record ResolveContext var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - Span pathBuffer = stackalloc byte[260]; + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; @@ -175,13 +177,21 @@ internal partial record ResolveContext var baseDirectory = modelPath[..modelPosition]; - baseDirectory.CopyTo(materialPathBuffer); - "/material/v"u8.CopyTo(materialPathBuffer[baseDirectory.Length..]); - WriteZeroPaddedNumber(materialPathBuffer.Slice(baseDirectory.Length + 11, 4), variant); - materialPathBuffer[baseDirectory.Length + 15] = (byte)'/'; - mtrlFileName.CopyTo(materialPathBuffer[(baseDirectory.Length + 16)..]); + var writer = new SpanTextWriter(materialPathBuffer); + writer.Append(baseDirectory); + writer.Append("/material/v"u8); + WriteZeroPaddedNumber(ref writer, 4, variant); + writer.Append((byte)'/'); + writer.Append(mtrlFileName); + writer.EnsureNullTerminated(); - return materialPathBuffer[..(baseDirectory.Length + 16 + mtrlFileName.Length)]; + return materialPathBuffer[..writer.Position]; + } + + private static void WriteZeroPaddedNumber(ref SpanTextWriter writer, int width, ushort number) + { + WriteZeroPaddedNumber(writer.GetRemainingSpan()[..width], number); + writer.Advance(width); } private static void WriteZeroPaddedNumber(Span destination, ushort number) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..29e15055 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -1,9 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; using OtterGui; +using OtterGui.Text.HelperObjects; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.GameData.Data; @@ -16,7 +16,7 @@ using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; using static Penumbra.Interop.Structs.StructExtensions; -using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; +using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; namespace Penumbra.Interop.ResourceTree; @@ -29,25 +29,25 @@ internal record GlobalResolveContext( { public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); - public unsafe ResolveContext CreateContext(CharacterBase* characterBase, uint slotIndex = 0xFFFFFFFFu, + public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } internal unsafe partial record ResolveContext( GlobalResolveContext Global, - Pointer CharacterBasePointer, + Pointer CharacterBasePointer, uint SlotIndex, EquipSlot Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { - public CharacterBase* CharacterBase + public CharaBase* CharacterBase => CharacterBasePointer.Value; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true); - private ModelType ModelType + private CharaBase.ModelType ModelType => CharacterBase->GetModelType(); private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) @@ -75,11 +75,14 @@ internal unsafe partial record ResolveContext( if (lastDirectorySeparator == -1 || lastDirectorySeparator > gamePath.Length - 3) return null; - Span prefixed = stackalloc byte[260]; - gamePath.Span[..(lastDirectorySeparator + 1)].CopyTo(prefixed); - prefixed[lastDirectorySeparator + 1] = (byte)'-'; - prefixed[lastDirectorySeparator + 2] = (byte)'-'; - gamePath.Span[(lastDirectorySeparator + 1)..].CopyTo(prefixed[(lastDirectorySeparator + 3)..]); + Span prefixed = stackalloc byte[CharaBase.PathBufferSize]; + + var writer = new SpanTextWriter(prefixed); + writer.Append(gamePath.Span[..(lastDirectorySeparator + 1)]); + writer.Append((byte)'-'); + writer.Append((byte)'-'); + writer.Append(gamePath.Span[(lastDirectorySeparator + 1)..]); + writer.EnsureNullTerminated(); if (!Utf8GamePath.FromSpan(prefixed[..(gamePath.Length + 2)], MetaDataComputation.None, out var tmp)) return null; From 243593e30f74c43a14b7d0ccdd9a264830158a59 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 19:52:39 +0200 Subject: [PATCH 317/865] RT: Fix VPR offhand material paths --- .../ResolveContext.PathResolution.cs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b99468f8..c3894b05 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -111,31 +111,25 @@ internal partial record ResolveContext if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; - // MNK (03??, 16??), NIN (18??) and DNC (26??) offhands share materials with the corresponding mainhand - if (setIdHigh is 3 or 16 or 18 or 26) + // Some offhands share materials with the corresponding mainhand + if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) { - var setIdLow = Equipment.Set.Id % 100; - if (setIdLow > 50) - { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); + var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - var mirroredSetId = (ushort)(Equipment.Set.Id - 50); + Span mirroredFileName = stackalloc byte[32]; + mirroredFileName = mirroredFileName[..fileName.Length]; + fileName.CopyTo(mirroredFileName); + WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId.Id); - Span mirroredFileName = stackalloc byte[32]; - mirroredFileName = mirroredFileName[..fileName.Length]; - fileName.CopyTo(mirroredFileName); - WriteZeroPaddedNumber(mirroredFileName[4..8], mirroredSetId); + Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; + pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); - Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, mirroredFileName); + var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); + if (weaponPosition >= 0) + WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId.Id); - var weaponPosition = pathBuffer.IndexOf("/weapon/w"u8); - if (weaponPosition >= 0) - WriteZeroPaddedNumber(pathBuffer[(weaponPosition + 9)..(weaponPosition + 13)], mirroredSetId); - - return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; - } + return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; } return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); From 75e3ef72f3dbaff37db0ba18d2770a5e7885f3ae Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 3 Aug 2024 20:27:16 +0200 Subject: [PATCH 318/865] RT: Fix Facewear --- .../ResolveContext.PathResolution.cs | 16 ++++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 30 ++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index c3894b05..43324516 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -16,20 +16,26 @@ namespace Penumbra.Interop.ResourceTree; internal partial record ResolveContext { + private static bool IsEquipmentOrAccessorySlot(uint slotIndex) + => slotIndex is < 10 or 16 or 17; + + private static bool IsEquipmentSlot(uint slotIndex) + => slotIndex is < 5 or 16 or 17; + private Utf8GamePath ResolveModelPath() { // Correctness: // Resolving a model path through the game's code can use EQDP metadata for human equipment models. return ModelType switch { - ModelType.Human when SlotIndex < 10 => ResolveEquipmentModelPath(), - _ => ResolveModelPathNative(), + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) => ResolveEquipmentModelPath(), + _ => ResolveModelPathNative(), }; } private Utf8GamePath ResolveEquipmentModelPath() { - var path = SlotIndex < 5 + var path = IsEquipmentSlot(SlotIndex) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; @@ -41,7 +47,7 @@ internal partial record ResolveContext private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { var slotIndex = slot.ToIndex(); - if (slotIndex >= 10 || ModelType != ModelType.Human) + if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human) return GenderRace.MidlanderMale; var characterRaceCode = (GenderRace)((Human*)CharacterBase)->RaceSexId; @@ -82,7 +88,7 @@ internal partial record ResolveContext // Resolving a material path through the game's code can dereference null pointers for materials that involve IMC metadata. return ModelType switch { - ModelType.Human when SlotIndex is < 10 or 16 && mtrlFileName[8] != (byte)'b' + ModelType.Human when IsEquipmentOrAccessorySlot(SlotIndex) && mtrlFileName[8] != (byte)'b' => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 6663fb40..f1507294 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -9,6 +9,7 @@ using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; +using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; namespace Penumbra.Interop.ResourceTree; @@ -44,8 +45,8 @@ public class ResourceTree PlayerRelated = playerRelated; CollectionName = collectionName; AnonymizedCollectionName = anonymizedCollectionName; - Nodes = new List(); - FlatNodes = new HashSet(); + Nodes = []; + FlatNodes = []; } public void ProcessPostfix(Action action) @@ -59,13 +60,13 @@ public class ResourceTree var character = (Character*)GameObjectAddress; var model = (CharacterBase*)DrawObjectAddress; var modelType = model->GetModelType(); - var human = modelType == CharacterBase.ModelType.Human ? (Human*)model : null; + var human = modelType == ModelType.Human ? (Human*)model : null; var equipment = modelType switch { - CharacterBase.ModelType.Human => new ReadOnlySpan(&human->Head, 10), - CharacterBase.ModelType.DemiHuman => new ReadOnlySpan( + ModelType.Human => new ReadOnlySpan(&human->Head, 12), + ModelType.DemiHuman => new ReadOnlySpan( Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), - _ => ReadOnlySpan.Empty, + _ => [], }; ModelId = character->CharacterData.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; @@ -75,9 +76,18 @@ public class ResourceTree for (var i = 0u; i < model->SlotCount; ++i) { - var slotContext = i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) - : globalContext.CreateContext(model, i); + var slotContext = modelType switch + { + ModelType.Human => i switch + { + < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]), + 16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]), + _ => globalContext.CreateContext(model, i), + }, + _ => i < equipment.Length + ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + : globalContext.CreateContext(model, i), + }; var imc = (ResourceHandle*)model->IMCArray[i]; var imcNode = slotContext.CreateNodeFromImc(imc); @@ -117,7 +127,7 @@ public class ResourceTree var subObject = (CharacterBase*)baseSubObject; - if (subObject->GetModelType() != CharacterBase.ModelType.Weapon) + if (subObject->GetModelType() != ModelType.Weapon) continue; var weapon = (Weapon*)subObject; From da3f3b8df39c24a84b92d34ee730e7ffc74abe84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 3 Aug 2024 22:45:38 +0200 Subject: [PATCH 319/865] Start rework of identified objects. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/CollectionApi.cs | 2 +- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/Api/Api/UiApi.cs | 10 +- .../Api/IpcTester/CollectionsIpcTester.cs | 16 +- Penumbra/Collections/Cache/CollectionCache.cs | 25 +- .../Collections/ModCollection.Cache.Access.cs | 6 +- Penumbra/Communication/ChangedItemClick.cs | 3 +- Penumbra/Communication/ChangedItemHover.cs | 3 +- Penumbra/EphemeralConfig.cs | 36 +- .../Interop/ResourceTree/ResolveContext.cs | 12 +- Penumbra/Interop/ResourceTree/ResourceNode.cs | 12 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 6 +- .../ResourceTree/ResourceTreeApiHelper.cs | 4 +- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 2 +- .../Manipulations/GlobalEqpManipulation.cs | 2 +- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- .../Meta/Manipulations/IMetaIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 4 +- Penumbra/Meta/Manipulations/Rsp.cs | 3 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 5 +- Penumbra/Mods/Mod.cs | 3 +- Penumbra/Penumbra.cs | 9 +- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 20 +- Penumbra/UI/ChangedItemDrawer.cs | 354 +++++------------- Penumbra/UI/ChangedItemIconFlag.cs | 122 ++++++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 11 +- .../UI/ModsTab/ModSearchStringSplitter.cs | 12 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 9 +- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- Penumbra/Util/IdentifierExtensions.cs | 10 +- 41 files changed, 342 insertions(+), 389 deletions(-) create mode 100644 Penumbra/UI/ChangedItemIconFlag.cs diff --git a/Penumbra.Api b/Penumbra.Api index 86249598..759a8e9d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 86249598afb71601b247f9629d9c29dbecfe6eb1 +Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0 diff --git a/Penumbra.GameData b/Penumbra.GameData index 75582ece..44427ad0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 75582ece58e6ee311074ff4ecaa68b804677878c +Subproject commit 44427ad0149059ab5ccb4e4a2f42a1a43423e4c5 diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index ff393aaf..04299187 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -36,7 +36,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : collection = ModCollection.Empty; if (collection.HasCache) - return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); + return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2?.ToInternalObject()); Penumbra.Log.Warning($"Collection {collectionId} does not exist or is not loaded."); return []; diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 60b00d37..790121d5 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -134,6 +134,6 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public Dictionary GetChangedItems(string modDirectory, string modName) => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) + ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject()) : []; } diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index cf3cd8f2..515874c0 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -1,7 +1,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; @@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - private void OnChangedItemClick(MouseButton button, object? data) + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) { if (ChangedItemClicked == null) return; - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); ChangedItemClicked.Invoke(button, type, id); } - private void OnChangedItemHover(object? data) + private void OnChangedItemHover(IIdentifiedObjectData? data) { if (ChangedItemTooltip == null) return; - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(data); + var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); ChangedItemTooltip.Invoke(type, id); } } diff --git a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs index 026fabbc..1d516eba 100644 --- a/Penumbra/Api/IpcTester/CollectionsIpcTester.cs +++ b/Penumbra/Api/IpcTester/CollectionsIpcTester.cs @@ -8,7 +8,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; -using Penumbra.GameData.Enums; +using Penumbra.GameData.Data; using ImGuiClip = OtterGui.ImGuiClip; namespace Penumbra.Api.IpcTester; @@ -17,10 +17,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService { private int _objectIdx; private string _collectionIdString = string.Empty; - private Guid? _collectionId = null; - private bool _allowCreation = true; - private bool _allowDeletion = true; - private ApiCollectionType _type = ApiCollectionType.Yourself; + private Guid? _collectionId; + private bool _allowCreation = true; + private bool _allowDeletion = true; + private ApiCollectionType _type = ApiCollectionType.Yourself; private Dictionary _collections = []; private (string, ChangedItemType, uint)[] _changedItems = []; @@ -116,7 +116,7 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService var items = new GetChangedItemsForCollection(pi).Invoke(_collectionId.GetValueOrDefault(Guid.Empty)); _changedItems = items.Select(kvp => { - var (type, id) = ChangedItemExtensions.ChangedItemToTypeAndId(kvp.Value); + var (type, id) = kvp.Value.ToApiObject(); return (kvp.Key, type, id); }).ToArray(); ImGui.OpenPopup("Changed Item List"); @@ -130,9 +130,9 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService if (!p) return; - using (var t = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) + using (var table = ImRaii.Table("##ChangedItems", 3, ImGuiTableFlags.SizingFixedFit)) { - if (t) + if (table) ImGuiClip.ClippedDraw(_changedItems, t => { ImGuiUtil.DrawTableColumn(t.Item1); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 4755840e..abc0dff8 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -6,6 +6,7 @@ using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Util; +using Penumbra.GameData.Data; namespace Penumbra.Collections.Cache; @@ -18,14 +19,14 @@ public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, /// public sealed class CollectionCache : IDisposable { - private readonly CollectionCacheManager _manager; - private readonly ModCollection _collection; - public readonly CollectionModData ModData = new(); - private readonly SortedList, object?)> _changedItems = []; - public readonly ConcurrentDictionary ResolvedFiles = new(); - public readonly CustomResourceCache CustomResources; - public readonly MetaCache Meta; - public readonly Dictionary> ConflictDict = []; + private readonly CollectionCacheManager _manager; + private readonly ModCollection _collection; + public readonly CollectionModData ModData = new(); + private readonly SortedList, IIdentifiedObjectData?)> _changedItems = []; + public readonly ConcurrentDictionary ResolvedFiles = new(); + public readonly CustomResourceCache CustomResources; + public readonly MetaCache Meta; + public readonly Dictionary> ConflictDict = []; public int Calculating = -1; @@ -41,7 +42,7 @@ public sealed class CollectionCache : IDisposable private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, object?)> ChangedItems + public IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems { get { @@ -412,7 +413,7 @@ public sealed class CollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = _manager.MetaFileManager.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { @@ -421,8 +422,8 @@ public sealed class CollectionCache : IDisposable if (!_changedItems.TryGetValue(name, out var data)) _changedItems.Add(name, (new SingleArray(mod), obj)); else if (!data.Item1.Contains(mod)) - _changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj); - else if (obj is int x && data.Item2 is int y) + _changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); + else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y) _changedItems[name] = (data.Item1, x + y); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 983509a4..0b38dde8 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -1,8 +1,8 @@ using OtterGui.Classes; using Penumbra.Mods; -using Penumbra.Meta.Files; using Penumbra.String.Classes; using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; using Penumbra.Mods.Editor; namespace Penumbra.Collections; @@ -46,8 +46,8 @@ public partial class ModCollection internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); - internal IReadOnlyDictionary, object?)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, object?)>(); + internal IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData?)>(); internal IEnumerable> AllConflicts => _cache?.AllConflicts ?? Array.Empty>(); diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 554e2221..1aac4454 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,6 +1,7 @@ using OtterGui.Classes; using Penumbra.Api.Api; using Penumbra.Api.Enums; +using Penumbra.GameData.Data; namespace Penumbra.Communication; @@ -11,7 +12,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 2dcced35..4e72b558 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -1,5 +1,6 @@ using OtterGui.Classes; using Penumbra.Api.Api; +using Penumbra.GameData.Data; namespace Penumbra.Communication; @@ -9,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 7457c910..24ab466b 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -23,24 +23,24 @@ public class EphemeralConfig : ISavable, IDisposable, IService [JsonIgnore] private readonly ModPathChanged _modPathChanged; - public int Version { get; set; } = Configuration.Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; - public bool DebugSeparateWindow { get; set; } = false; - public int TutorialStep { get; set; } = 0; - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public bool OnlyAddMatchingResources { get; set; } = true; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; - public TabType SelectedTab { get; set; } = TabType.Settings; - public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags; - public bool FixMainWindow { get; set; } = false; - public string LastModPath { get; set; } = string.Empty; - public bool AdvancedEditingOpen { get; set; } = false; - public bool ForceRedrawOnFileChange { get; set; } = false; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public bool DebugSeparateWindow { get; set; } = false; + public int TutorialStep { get; set; } = 0; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public bool OnlyAddMatchingResources { get; set; } = true; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; + public bool FixMainWindow { get; set; } = false; + public string LastModPath { get; set; } = string.Empty; + public bool AdvancedEditingOpen { get; set; } = false; + public bool ForceRedrawOnFileChange { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 3fc1ae3c..9d0f1e46 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -345,7 +345,7 @@ internal unsafe partial record ResolveContext( _ => string.Empty, } + item.Name; - return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(item.Name, item)); + return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag()); } var dataFromPath = GuessUiDataFromPath(gamePath); @@ -353,8 +353,8 @@ internal unsafe partial record ResolveContext( return dataFromPath; return isEquipment - ? new ResourceNode.UiData(Slot.ToName(), ChangedItemDrawer.GetCategoryIcon(Slot.ToSlot())) - : new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + ? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag()) + : new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath) @@ -362,13 +362,13 @@ internal unsafe partial record ResolveContext( foreach (var obj in Global.Identifier.Identify(gamePath.ToString())) { var name = obj.Key; - if (name.StartsWith("Customization:")) + if (obj.Value is IdentifiedCustomization) name = name[14..].Trim(); if (name != "Unknown") - return new ResourceNode.UiData(name, ChangedItemDrawer.GetCategoryIcon(obj.Key, obj.Value)); + return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } - return new ResourceNode.UiData(null, ChangedItemDrawer.ChangedItemIcon.Unknown); + return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } private static string? SafeGet(ReadOnlySpan array, Index index) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index de43a874..6ab48325 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,7 +1,7 @@ using Penumbra.Api.Enums; using Penumbra.String; using Penumbra.String.Classes; -using ChangedItemIcon = Penumbra.UI.ChangedItemDrawer.ChangedItemIcon; +using Penumbra.UI; namespace Penumbra.Interop.ResourceTree; @@ -9,7 +9,7 @@ public class ResourceNode : ICloneable { public string? Name; public string? FallbackName; - public ChangedItemIcon Icon; + public ChangedItemIconFlag IconFlag; public readonly ResourceType Type; public readonly nint ObjectAddress; public readonly nint ResourceHandle; @@ -51,7 +51,7 @@ public class ResourceNode : ICloneable { Name = other.Name; FallbackName = other.FallbackName; - Icon = other.Icon; + IconFlag = other.IconFlag; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; @@ -79,7 +79,7 @@ public class ResourceNode : ICloneable public void SetUiData(UiData uiData) { Name = uiData.Name; - Icon = uiData.Icon; + IconFlag = uiData.IconFlag; } public void PrependName(string prefix) @@ -88,9 +88,9 @@ public class ResourceNode : ICloneable Name = prefix + Name; } - public readonly record struct UiData(string? Name, ChangedItemIcon Icon) + public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag) { public UiData PrependName(string prefix) - => Name == null ? this : new UiData(prefix + Name, Icon); + => Name == null ? this : new UiData(prefix + Name, IconFlag); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 6663fb40..dc83fa65 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -174,7 +174,7 @@ public class ResourceTree { pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(pbdNode); @@ -192,7 +192,7 @@ public class ResourceTree { decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; - decalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + decalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(decalNode); @@ -209,7 +209,7 @@ public class ResourceTree { legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; - legacyDecalNode.Icon = ChangedItemDrawer.ChangedItemIcon.Customization; + legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(legacyDecalNode); diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 22025dd6..48690e98 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -67,7 +67,7 @@ internal static class ResourceTreeApiHelper continue; var fullPath = node.FullPath.ToPath(); - resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)ChangedItemDrawer.ToApiIcon(node.Icon))); + resDictionary.Add(node.ResourceHandle, (fullPath, node.Name ?? string.Empty, (uint)node.IconFlag.ToApiIcon())); } } @@ -106,7 +106,7 @@ internal static class ResourceTreeApiHelper var ret = new JObject { [nameof(ResourceNodeDto.Type)] = new JValue(node.Type), - [nameof(ResourceNodeDto.Icon)] = new JValue(ChangedItemDrawer.ToApiIcon(node.Icon)), + [nameof(ResourceNodeDto.Icon)] = new JValue(node.IconFlag.ToApiIcon()), [nameof(ResourceNodeDto.Name)] = node.Name, [nameof(ResourceNodeDto.GamePath)] = node.GamePath.Equals(Utf8GamePath.Empty) ? null : node.GamePath.ToString(), [nameof(ResourceNodeDto.ActualPath)] = node.FullPath.ToString(), diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 3f856bd2..3a804d0c 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 5d37aac8..f758126c 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 2955dba4..cfe9b7d4 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -24,7 +24,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { switch (Slot) { diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 2b88d962..ec59762b 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = Type switch { diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index a6fcf58b..1f41adfb 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 5707ffca..d1668a4d 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -18,7 +18,7 @@ public enum MetaManipulationType : byte public interface IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); public MetaIndex FileIndex(); diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index d4887fe2..1b2492ee 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 73d1d7e5..2d73ec7f 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -1,4 +1,3 @@ -using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -9,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); public MetaIndex FileIndex() diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 9327ced9..c5654019 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -45,7 +45,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 7b0eb094..d42804ba 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -121,7 +121,7 @@ public class ImcModGroup(Mod mod) : IModGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index ee27d534..9cf7e6a3 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -126,7 +126,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index cc606f42..723cd5b1 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -111,7 +111,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 2c292a14..1a2f2798 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -186,7 +186,7 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, @@ -255,7 +255,8 @@ public static class EquipmentSwap { items = identifier.Identify(slotFrom.IsEquipment() ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType() + : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) + .Select(kvp => kvp.Value).OfType().Select(i => i.Item) .ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 16f06de2..488e3dc1 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -100,7 +101,7 @@ public sealed class Mod : IMod } // Cache - public readonly SortedList ChangedItems = new(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 8ea74987..dbe06803 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,8 @@ using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using System.Xml.Linq; +using Dalamud.Plugin.Services; +using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; @@ -109,16 +111,17 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { _services.GetService(); + var itemSheet = _services.GetService().GetExcelSheet()!; _communicatorService.ChangedItemHover.Subscribe(it => { - if (it is (Item, FullEquipType)) + if (it is IdentifiedItem) ImGui.TextUnformatted("Left Click to create an item link in chat."); }, ChangedItemHover.Priority.Link); _communicatorService.ChangedItemClick.Subscribe((button, it) => { - if (button == MouseButton.Left && it is (Item item, FullEquipType type)) - Messager.LinkItem(item); + if (button == MouseButton.Left && it is IdentifiedItem item && itemSheet.GetRow(item.Item.ItemId.Id) is { } i) + Messager.LinkItem(i); }, ChangedItemClick.Priority.Link); } diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 70b05a73..5ba57cf4 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -115,7 +115,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; - _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() + _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() ?? _config.Ephemeral.ChangedItemFilter; _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; _config.Ephemeral.Save(); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index da2daeb7..b75c5aef 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -135,7 +135,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService : FilterComboCache<(EquipItem Item, bool InMod)>(() => { var list = data.ByType[type]; - if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is EquipItem i && i.Type == type)) + if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)) return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); return list.Select(i => (i, false)).ToList(); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index c47414b9..9834d9f0 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -27,7 +27,7 @@ public class ResourceTreeViewer private readonly Dictionary _filterCache; private TreeCategory _categoryFilter; - private ChangedItemDrawer.ChangedItemIcon _typeFilter; + private ChangedItemIconFlag _typeFilter; private string _nameFilter; private string _nodeFilter; @@ -48,7 +48,7 @@ public class ResourceTreeViewer _filterCache = []; _categoryFilter = AllCategories; - _typeFilter = ChangedItemDrawer.AllFlags; + _typeFilter = ChangedItemFlagExtensions.AllFlags; _nameFilter = string.Empty; _nodeFilter = string.Empty; } @@ -185,13 +185,13 @@ public class ResourceTreeViewer }); private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, - ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + ChangedItemIconFlag parentFilterIconFlag) { var debugMode = _config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - bool MatchesFilter(ResourceNode node, ChangedItemDrawer.ChangedItemIcon filterIcon) + bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) { if (!_typeFilter.HasFlag(filterIcon)) return false; @@ -205,12 +205,12 @@ public class ResourceTreeViewer || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); } - NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) { if (node.Internal && !debugMode) return NodeVisibility.Hidden; - var filterIcon = node.Icon != 0 ? node.Icon : parentFilterIcon; + var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; if (MatchesFilter(node, filterIcon)) return NodeVisibility.Visible; @@ -223,7 +223,7 @@ public class ResourceTreeViewer return NodeVisibility.Hidden; } - NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemDrawer.ChangedItemIcon parentFilterIcon) + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) { if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) { @@ -241,7 +241,7 @@ public class ResourceTreeViewer { var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); - var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIcon); + var visibility = GetNodeVisibility(nodePathHash, resourceNode, parentFilterIconFlag); if (visibility == NodeVisibility.Hidden) continue; @@ -250,7 +250,7 @@ public class ResourceTreeViewer using var mutedColor = ImRaii.PushColor(ImGuiCol.Text, textColorInternal, resourceNode.Internal); - var filterIcon = resourceNode.Icon != 0 ? resourceNode.Icon : parentFilterIcon; + var filterIcon = resourceNode.IconFlag != 0 ? resourceNode.IconFlag : parentFilterIconFlag; using var id = ImRaii.PushId(index); ImGui.TableNextColumn(); @@ -281,7 +281,7 @@ public class ResourceTreeViewer ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } - _changedItemDrawer.DrawCategoryIcon(resourceNode.Icon); + _changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && unfoldable) diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index 72bfa266..af9782d5 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -3,72 +3,24 @@ using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using ImGuiNET; using Lumina.Data.Files; -using Lumina.Excel; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Structs; +using Penumbra.GameData.Data; using Penumbra.Services; using Penumbra.UI.Classes; -using ApiChangedItemIcon = Penumbra.Api.Enums.ChangedItemIcon; namespace Penumbra.UI; public class ChangedItemDrawer : IDisposable, IUiService { - [Flags] - public enum ChangedItemIcon : uint - { - Head = 0x00_00_01, - Body = 0x00_00_02, - Hands = 0x00_00_04, - Legs = 0x00_00_08, - Feet = 0x00_00_10, - Ears = 0x00_00_20, - Neck = 0x00_00_40, - Wrists = 0x00_00_80, - Finger = 0x00_01_00, - Monster = 0x00_02_00, - Demihuman = 0x00_04_00, - Customization = 0x00_08_00, - Action = 0x00_10_00, - Mainhand = 0x00_20_00, - Offhand = 0x00_40_00, - Unknown = 0x00_80_00, - Emote = 0x01_00_00, - } + private static readonly string[] LowerNames = ChangedItemFlagExtensions.Order.Select(f => f.ToDescription().ToLowerInvariant()).ToArray(); - private static readonly ChangedItemIcon[] Order = - [ - ChangedItemIcon.Head, - ChangedItemIcon.Body, - ChangedItemIcon.Hands, - ChangedItemIcon.Legs, - ChangedItemIcon.Feet, - ChangedItemIcon.Ears, - ChangedItemIcon.Neck, - ChangedItemIcon.Wrists, - ChangedItemIcon.Finger, - ChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand, - ChangedItemIcon.Customization, - ChangedItemIcon.Action, - ChangedItemIcon.Emote, - ChangedItemIcon.Monster, - ChangedItemIcon.Demihuman, - ChangedItemIcon.Unknown, - ]; - - private static readonly string[] LowerNames = Order.Select(f => ToDescription(f).ToLowerInvariant()).ToArray(); - - public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIcon slot) + public static bool TryParseIndex(ReadOnlySpan input, out ChangedItemIconFlag slot) { // Handle numeric cases before TryParse because numbers // are not logical otherwise. @@ -77,15 +29,15 @@ public class ChangedItemDrawer : IDisposable, IUiService // We assume users will use 1-based index, but if they enter 0, just use the first. if (idx == 0) { - slot = Order[0]; + slot = ChangedItemFlagExtensions.Order[0]; return true; } // Use 1-based index. --idx; - if (idx >= 0 && idx < Order.Length) + if (idx >= 0 && idx < ChangedItemFlagExtensions.Order.Count) { - slot = Order[idx]; + slot = ChangedItemFlagExtensions.Order[idx]; return true; } } @@ -94,13 +46,13 @@ public class ChangedItemDrawer : IDisposable, IUiService return false; } - public static bool TryParsePartial(string lowerInput, out ChangedItemIcon slot) + public static bool TryParsePartial(string lowerInput, out ChangedItemIconFlag slot) { if (TryParseIndex(lowerInput, out slot)) return true; slot = 0; - foreach (var (item, flag) in LowerNames.Zip(Order)) + foreach (var (item, flag) in LowerNames.Zip(ChangedItemFlagExtensions.Order)) { if (item.Contains(lowerInput, StringComparison.Ordinal)) slot |= flag; @@ -109,15 +61,11 @@ public class ChangedItemDrawer : IDisposable, IUiService return slot != 0; } - public const ChangedItemIcon AllFlags = (ChangedItemIcon)0x01FFFF; - public static readonly int NumCategories = Order.Length; - public const ChangedItemIcon DefaultFlags = AllFlags & ~ChangedItemIcon.Offhand; - private readonly Configuration _config; - private readonly ExcelSheet _items; - private readonly CommunicatorService _communicator; - private readonly Dictionary _icons = new(16); - private float _smallestIconWidth; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly Dictionary _icons = new(16); + private float _smallestIconWidth; public static Vector2 TypeFilterIconSize => new(2 * ImGui.GetTextLineHeight()); @@ -125,7 +73,6 @@ public class ChangedItemDrawer : IDisposable, IUiService public ChangedItemDrawer(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider, CommunicatorService communicator, Configuration config) { - _items = gameData.GetExcelSheet()!; uiBuilder.RunWhenUiPrepared(() => CreateEquipSlotIcons(uiBuilder, gameData, textureProvider), true); _communicator = communicator; _config = config; @@ -139,18 +86,19 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, object? data, LowerString filter) - => (_config.Ephemeral.ChangedItemFilter == AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(GetCategoryIcon(name, data))) - && (filter.IsEmpty || filter.IsContained(ChangedItemFilterName(name, data))); + public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) + => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags + || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) + && (filter.IsEmpty || !data.IsFilteredOut(name, filter)); /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(string name, object? data) - => DrawCategoryIcon(GetCategoryIcon(name, data)); + public void DrawCategoryIcon(IIdentifiedObjectData? data) + => DrawCategoryIcon(data.GetIcon().ToFlag()); - public void DrawCategoryIcon(ChangedItemIcon iconType) + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) { var height = ImGui.GetFrameHeight(); - if (!_icons.TryGetValue(iconType, out var icon)) + if (!_icons.TryGetValue(iconFlagType, out var icon)) { ImGui.Dummy(new Vector2(height)); return; @@ -162,18 +110,18 @@ public class ChangedItemDrawer : IDisposable, IUiService using var tt = ImRaii.Tooltip(); ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); - ImGuiUtil.DrawTextButton(ToDescription(iconType), new Vector2(0, _smallestIconWidth), 0); + ImGuiUtil.DrawTextButton(iconFlagType.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } } /// /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item Id in grey if requested. + /// Also draw the item ID in grey if requested. /// - public void DrawChangedItem(string name, object? data) + public void DrawChangedItem(string name, IIdentifiedObjectData? data) { - name = ChangedItemName(name, data); - using (var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) + name = data?.ToName(name) ?? name; + using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) { var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) @@ -182,31 +130,34 @@ public class ChangedItemDrawer : IDisposable, IUiService ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, Convert(data)); + _communicator.ChangedItemClick.Invoke(ret, data); } if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) { // We can not be sure that any subscriber actually prints something in any case. // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using var group = ImRaii.Group(); - _communicator.ChangedItemHover.Invoke(Convert(data)); - group.Dispose(); + using var tt = ImRaii.Tooltip(); + using (ImRaii.Group()) + { + _communicator.ChangedItemHover.Invoke(data); + } + if (ImGui.GetItemRectSize() == Vector2.Zero) ImGui.TextUnformatted("No actions available."); } } /// Draw the model information, right-justified. - public void DrawModelData(object? data) + public static void DrawModelData(IIdentifiedObjectData? data) { - if (!GetChangedItemObject(data, out var text)) + var additionalData = data?.AdditionalData ?? string.Empty; + if (additionalData.Length == 0) return; ImGui.SameLine(ImGui.GetContentRegionAvail().X); ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightJustify(text, ColorId.ItemId.Value()); + ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); } /// Draw a header line with the different icon types to filter them. @@ -224,7 +175,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Draw a header line with the different icon types to filter them. - public bool DrawTypeFilter(ref ChangedItemIcon typeFilter) + public bool DrawTypeFilter(ref ChangedItemIconFlag typeFilter) { var ret = false; using var _ = ImRaii.PushId("ChangedItemIconFilter"); @@ -232,16 +183,38 @@ public class ChangedItemDrawer : IDisposable, IUiService using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - bool DrawIcon(ChangedItemIcon type, ref ChangedItemIcon typeFilter) + foreach (var iconType in ChangedItemFlagExtensions.Order) { - var ret = false; - var icon = _icons[type]; - var flag = typeFilter.HasFlag(type); + ret |= DrawIcon(iconType, ref typeFilter); + ImGui.SameLine(); + } + + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); + ImGui.Image(_icons[ChangedItemFlagExtensions.AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, + typeFilter switch + { + 0 => new Vector4(0.6f, 0.3f, 0.3f, 1f), + ChangedItemFlagExtensions.AllFlags => new Vector4(0.75f, 0.75f, 0.75f, 1f), + _ => new Vector4(0.5f, 0.5f, 1f, 1f), + }); + if (ImGui.IsItemClicked()) + { + typeFilter = typeFilter == ChangedItemFlagExtensions.AllFlags ? 0 : ChangedItemFlagExtensions.AllFlags; + ret = true; + } + + return ret; + + bool DrawIcon(ChangedItemIconFlag type, ref ChangedItemIconFlag typeFilter) + { + 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)); if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { typeFilter = flag ? typeFilter & ~type : typeFilter | type; - ret = true; + localRet = true; } using var popup = ImRaii.ContextPopupItem(type.ToString()); @@ -249,7 +222,7 @@ public class ChangedItemDrawer : IDisposable, IUiService if (ImGui.MenuItem("Enable Only This")) { typeFilter = type; - ret = true; + localRet = true; ImGui.CloseCurrentPopup(); } @@ -258,165 +231,13 @@ public class ChangedItemDrawer : IDisposable, IUiService using var tt = ImRaii.Tooltip(); ImGui.Image(icon.ImGuiHandle, new Vector2(_smallestIconWidth)); ImGui.SameLine(); - ImGuiUtil.DrawTextButton(ToDescription(type), new Vector2(0, _smallestIconWidth), 0); + ImGuiUtil.DrawTextButton(type.ToDescription(), new Vector2(0, _smallestIconWidth), 0); } - return ret; - } - - foreach (var iconType in Order) - { - ret |= DrawIcon(iconType, ref typeFilter); - ImGui.SameLine(); - } - - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - size.X); - ImGui.Image(_icons[AllFlags].ImGuiHandle, size, Vector2.Zero, Vector2.One, - typeFilter == 0 ? new Vector4(0.6f, 0.3f, 0.3f, 1f) : - typeFilter == AllFlags ? new Vector4(0.75f, 0.75f, 0.75f, 1f) : new Vector4(0.5f, 0.5f, 1f, 1f)); - if (ImGui.IsItemClicked()) - { - typeFilter = typeFilter == AllFlags ? 0 : AllFlags; - ret = true; - } - - return ret; - } - - /// Obtain the icon category corresponding to a changed item. - internal static ChangedItemIcon GetCategoryIcon(string name, object? obj) - { - var iconType = ChangedItemIcon.Unknown; - switch (obj) - { - case EquipItem it: - iconType = GetCategoryIcon(it.Type.ToSlot()); - break; - case ModelChara m: - iconType = (CharacterBase.ModelType)m.Type switch - { - CharacterBase.ModelType.DemiHuman => ChangedItemIcon.Demihuman, - CharacterBase.ModelType.Monster => ChangedItemIcon.Monster, - _ => ChangedItemIcon.Unknown, - }; - break; - default: - { - if (name.StartsWith("Action: ")) - iconType = ChangedItemIcon.Action; - else if (name.StartsWith("Emote: ")) - iconType = ChangedItemIcon.Emote; - else if (name.StartsWith("Customization: ")) - iconType = ChangedItemIcon.Customization; - break; - } - } - - return iconType; - } - - internal static ChangedItemIcon GetCategoryIcon(EquipSlot slot) - => slot switch - { - EquipSlot.MainHand => ChangedItemIcon.Mainhand, - EquipSlot.OffHand => ChangedItemIcon.Offhand, - EquipSlot.Head => ChangedItemIcon.Head, - EquipSlot.Body => ChangedItemIcon.Body, - EquipSlot.Hands => ChangedItemIcon.Hands, - EquipSlot.Legs => ChangedItemIcon.Legs, - EquipSlot.Feet => ChangedItemIcon.Feet, - EquipSlot.Ears => ChangedItemIcon.Ears, - EquipSlot.Neck => ChangedItemIcon.Neck, - EquipSlot.Wrists => ChangedItemIcon.Wrists, - EquipSlot.RFinger => ChangedItemIcon.Finger, - _ => ChangedItemIcon.Unknown, - }; - - /// Return more detailed object information in text, if it exists. - private static bool GetChangedItemObject(object? obj, out string text) - { - switch (obj) - { - case EquipItem it: - text = it.ModelString; - return true; - case ModelChara m: - text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; - return true; - default: - text = string.Empty; - return false; + return localRet; } } - /// We need to transform the internal EquipItem type to the Lumina Item type for API-events. - private object? Convert(object? data) - { - if (data is EquipItem it) - return (_items.GetRow(it.ItemId.Id), it.Type); - - return data; - } - - private static string ToDescription(ChangedItemIcon icon) - => icon switch - { - ChangedItemIcon.Head => EquipSlot.Head.ToName(), - ChangedItemIcon.Body => EquipSlot.Body.ToName(), - ChangedItemIcon.Hands => EquipSlot.Hands.ToName(), - ChangedItemIcon.Legs => EquipSlot.Legs.ToName(), - ChangedItemIcon.Feet => EquipSlot.Feet.ToName(), - ChangedItemIcon.Ears => EquipSlot.Ears.ToName(), - ChangedItemIcon.Neck => EquipSlot.Neck.ToName(), - ChangedItemIcon.Wrists => EquipSlot.Wrists.ToName(), - ChangedItemIcon.Finger => "Ring", - ChangedItemIcon.Monster => "Monster", - ChangedItemIcon.Demihuman => "Demi-Human", - ChangedItemIcon.Customization => "Customization", - ChangedItemIcon.Action => "Action", - ChangedItemIcon.Emote => "Emote", - ChangedItemIcon.Mainhand => "Weapon (Mainhand)", - ChangedItemIcon.Offhand => "Weapon (Offhand)", - _ => "Other", - }; - - internal static ApiChangedItemIcon ToApiIcon(ChangedItemIcon icon) - => icon switch - { - ChangedItemIcon.Head => ApiChangedItemIcon.Head, - ChangedItemIcon.Body => ApiChangedItemIcon.Body, - ChangedItemIcon.Hands => ApiChangedItemIcon.Hands, - ChangedItemIcon.Legs => ApiChangedItemIcon.Legs, - ChangedItemIcon.Feet => ApiChangedItemIcon.Feet, - ChangedItemIcon.Ears => ApiChangedItemIcon.Ears, - ChangedItemIcon.Neck => ApiChangedItemIcon.Neck, - ChangedItemIcon.Wrists => ApiChangedItemIcon.Wrists, - ChangedItemIcon.Finger => ApiChangedItemIcon.Finger, - ChangedItemIcon.Monster => ApiChangedItemIcon.Monster, - ChangedItemIcon.Demihuman => ApiChangedItemIcon.Demihuman, - ChangedItemIcon.Customization => ApiChangedItemIcon.Customization, - ChangedItemIcon.Action => ApiChangedItemIcon.Action, - ChangedItemIcon.Emote => ApiChangedItemIcon.Emote, - ChangedItemIcon.Mainhand => ApiChangedItemIcon.Mainhand, - ChangedItemIcon.Offhand => ApiChangedItemIcon.Offhand, - ChangedItemIcon.Unknown => ApiChangedItemIcon.Unknown, - _ => ApiChangedItemIcon.None, - }; - - /// Apply Changed Item Counters to the Name if necessary. - private static string ChangedItemName(string name, object? data) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; - - /// Add filterable information to the string. - private static string ChangedItemFilterName(string name, object? data) - => data switch - { - int counter => $"{counter} Files Manipulating {name}s", - EquipItem it => $"{name}\0{(GetChangedItemObject(it, out var t) ? t : string.Empty)}", - ModelChara m => $"{name}\0{(GetChangedItemObject(m, out var t) ? t : string.Empty)}", - _ => name, - }; - /// Initialize the icons. private bool CreateEquipSlotIcons(IUiBuilder uiBuilder, IDataManager gameData, ITextureProvider textureProvider) { @@ -425,30 +246,30 @@ public class ChangedItemDrawer : IDisposable, IUiService if (!equipTypeIcons.Valid) return false; - void Add(ChangedItemIcon icon, IDalamudTextureWrap? tex) + void Add(ChangedItemIconFlag icon, IDalamudTextureWrap? tex) { if (tex != null) _icons.Add(icon, tex); } - Add(ChangedItemIcon.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); - Add(ChangedItemIcon.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); - Add(ChangedItemIcon.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); - Add(ChangedItemIcon.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); - Add(ChangedItemIcon.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); - Add(ChangedItemIcon.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); - Add(ChangedItemIcon.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); - Add(ChangedItemIcon.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); - Add(ChangedItemIcon.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); - Add(ChangedItemIcon.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); - Add(ChangedItemIcon.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); - Add(ChangedItemIcon.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062042_hr1.tex")!)); - Add(ChangedItemIcon.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062041_hr1.tex")!)); - Add(ChangedItemIcon.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); - Add(ChangedItemIcon.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); - Add(ChangedItemIcon.Emote, LoadEmoteTexture(gameData, textureProvider)); - Add(ChangedItemIcon.Unknown, LoadUnknownTexture(gameData, textureProvider)); - Add(AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); + Add(ChangedItemIconFlag.Mainhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 0)); + Add(ChangedItemIconFlag.Head, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 1)); + Add(ChangedItemIconFlag.Body, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 2)); + Add(ChangedItemIconFlag.Hands, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 3)); + Add(ChangedItemIconFlag.Legs, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 5)); + Add(ChangedItemIconFlag.Feet, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 6)); + Add(ChangedItemIconFlag.Offhand, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 7)); + Add(ChangedItemIconFlag.Ears, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 8)); + Add(ChangedItemIconFlag.Neck, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 9)); + Add(ChangedItemIconFlag.Wrists, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 10)); + Add(ChangedItemIconFlag.Finger, equipTypeIcons.LoadTexturePart("ui/uld/ArmouryBoard_hr1.tex", 11)); + Add(ChangedItemIconFlag.Monster, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062044_hr1.tex")!)); + Add(ChangedItemIconFlag.Demihuman, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062043_hr1.tex")!)); + Add(ChangedItemIconFlag.Customization, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062045_hr1.tex")!)); + Add(ChangedItemIconFlag.Action, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/062000/062001_hr1.tex")!)); + Add(ChangedItemIconFlag.Emote, LoadEmoteTexture(gameData, textureProvider)); + Add(ChangedItemIconFlag.Unknown, LoadUnknownTexture(gameData, textureProvider)); + Add(ChangedItemFlagExtensions.AllFlags, textureProvider.CreateFromTexFile(gameData.GetFile("ui/icon/114000/114052_hr1.tex")!)); _smallestIconWidth = _icons.Values.Min(i => i.Width); @@ -487,6 +308,7 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, "Penumbra.EmoteItemIcon"); + return textureProvider.CreateFromRaw(RawImageSpecification.Rgba32(emote.Header.Width, emote.Header.Height), image2, + "Penumbra.EmoteItemIcon"); } } diff --git a/Penumbra/UI/ChangedItemIconFlag.cs b/Penumbra/UI/ChangedItemIconFlag.cs new file mode 100644 index 00000000..fc7073f2 --- /dev/null +++ b/Penumbra/UI/ChangedItemIconFlag.cs @@ -0,0 +1,122 @@ +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; + +namespace Penumbra.UI; + +[Flags] +public enum ChangedItemIconFlag : uint +{ + Head = 0x00_00_01, + Body = 0x00_00_02, + Hands = 0x00_00_04, + Legs = 0x00_00_08, + Feet = 0x00_00_10, + Ears = 0x00_00_20, + Neck = 0x00_00_40, + Wrists = 0x00_00_80, + Finger = 0x00_01_00, + Monster = 0x00_02_00, + Demihuman = 0x00_04_00, + Customization = 0x00_08_00, + Action = 0x00_10_00, + Mainhand = 0x00_20_00, + Offhand = 0x00_40_00, + Unknown = 0x00_80_00, + Emote = 0x01_00_00, +} + +public static class ChangedItemFlagExtensions +{ + public static readonly IReadOnlyList Order = + [ + ChangedItemIconFlag.Head, + ChangedItemIconFlag.Body, + ChangedItemIconFlag.Hands, + ChangedItemIconFlag.Legs, + ChangedItemIconFlag.Feet, + ChangedItemIconFlag.Ears, + ChangedItemIconFlag.Neck, + ChangedItemIconFlag.Wrists, + ChangedItemIconFlag.Finger, + ChangedItemIconFlag.Mainhand, + ChangedItemIconFlag.Offhand, + ChangedItemIconFlag.Customization, + ChangedItemIconFlag.Action, + ChangedItemIconFlag.Emote, + ChangedItemIconFlag.Monster, + ChangedItemIconFlag.Demihuman, + ChangedItemIconFlag.Unknown, + ]; + + public const ChangedItemIconFlag AllFlags = (ChangedItemIconFlag)0x01FFFF; + public static readonly int NumCategories = Order.Count; + public const ChangedItemIconFlag DefaultFlags = AllFlags & ~ChangedItemIconFlag.Offhand; + + public static string ToDescription(this ChangedItemIconFlag iconFlag) + => iconFlag switch + { + ChangedItemIconFlag.Head => EquipSlot.Head.ToName(), + ChangedItemIconFlag.Body => EquipSlot.Body.ToName(), + ChangedItemIconFlag.Hands => EquipSlot.Hands.ToName(), + ChangedItemIconFlag.Legs => EquipSlot.Legs.ToName(), + ChangedItemIconFlag.Feet => EquipSlot.Feet.ToName(), + ChangedItemIconFlag.Ears => EquipSlot.Ears.ToName(), + ChangedItemIconFlag.Neck => EquipSlot.Neck.ToName(), + ChangedItemIconFlag.Wrists => EquipSlot.Wrists.ToName(), + ChangedItemIconFlag.Finger => "Ring", + ChangedItemIconFlag.Monster => "Monster", + ChangedItemIconFlag.Demihuman => "Demi-Human", + ChangedItemIconFlag.Customization => "Customization", + ChangedItemIconFlag.Action => "Action", + ChangedItemIconFlag.Emote => "Emote", + ChangedItemIconFlag.Mainhand => "Weapon (Mainhand)", + ChangedItemIconFlag.Offhand => "Weapon (Offhand)", + _ => "Other", + }; + + public static ChangedItemIcon ToApiIcon(this ChangedItemIconFlag iconFlag) + => iconFlag switch + { + ChangedItemIconFlag.Head => ChangedItemIcon.Head, + ChangedItemIconFlag.Body => ChangedItemIcon.Body, + ChangedItemIconFlag.Hands => ChangedItemIcon.Hands, + ChangedItemIconFlag.Legs => ChangedItemIcon.Legs, + ChangedItemIconFlag.Feet => ChangedItemIcon.Feet, + ChangedItemIconFlag.Ears => ChangedItemIcon.Ears, + ChangedItemIconFlag.Neck => ChangedItemIcon.Neck, + ChangedItemIconFlag.Wrists => ChangedItemIcon.Wrists, + ChangedItemIconFlag.Finger => ChangedItemIcon.Finger, + ChangedItemIconFlag.Monster => ChangedItemIcon.Monster, + ChangedItemIconFlag.Demihuman => ChangedItemIcon.Demihuman, + ChangedItemIconFlag.Customization => ChangedItemIcon.Customization, + ChangedItemIconFlag.Action => ChangedItemIcon.Action, + ChangedItemIconFlag.Emote => ChangedItemIcon.Emote, + ChangedItemIconFlag.Mainhand => ChangedItemIcon.Mainhand, + ChangedItemIconFlag.Offhand => ChangedItemIcon.Offhand, + ChangedItemIconFlag.Unknown => ChangedItemIcon.Unknown, + _ => ChangedItemIcon.None, + }; + + public static ChangedItemIconFlag ToFlag(this ChangedItemIcon icon) + => icon switch + { + ChangedItemIcon.Unknown => ChangedItemIconFlag.Unknown, + ChangedItemIcon.Head => ChangedItemIconFlag.Head, + ChangedItemIcon.Body => ChangedItemIconFlag.Body, + ChangedItemIcon.Hands => ChangedItemIconFlag.Hands, + ChangedItemIcon.Legs => ChangedItemIconFlag.Legs, + ChangedItemIcon.Feet => ChangedItemIconFlag.Feet, + ChangedItemIcon.Ears => ChangedItemIconFlag.Ears, + ChangedItemIcon.Neck => ChangedItemIconFlag.Neck, + ChangedItemIcon.Wrists => ChangedItemIconFlag.Wrists, + ChangedItemIcon.Finger => ChangedItemIconFlag.Finger, + ChangedItemIcon.Mainhand => ChangedItemIconFlag.Mainhand, + ChangedItemIcon.Offhand => ChangedItemIconFlag.Offhand, + ChangedItemIcon.Customization => ChangedItemIconFlag.Customization, + ChangedItemIcon.Monster => ChangedItemIconFlag.Monster, + ChangedItemIcon.Demihuman => ChangedItemIconFlag.Demihuman, + ChangedItemIcon.Action => ChangedItemIconFlag.Action, + ChangedItemIcon.Emote => ChangedItemIconFlag.Emote, + _ => ChangedItemIconFlag.Unknown, + }; +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 55405313..42689efb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -550,7 +550,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector)selector.Selected!.ChangedItems); + var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); var height = ImGui.GetFrameHeightWithSpacing(); ImGui.TableNextColumn(); var skips = ImGuiClip.GetNecessarySkips(height); @@ -32,15 +33,15 @@ public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItem ImGuiClip.DrawEndDummy(remainder, height); } - private bool CheckFilter((string Name, object? Data) kvp) + private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); - private void DrawChangedItem((string Name, object? Data) kvp) + private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(kvp.Name, kvp.Data); + drawer.DrawCategoryIcon(kvp.Data); ImGui.SameLine(); drawer.DrawChangedItem(kvp.Name, kvp.Data); - drawer.DrawModelData(kvp.Data); + ChangedItemDrawer.DrawModelData(kvp.Data); } } diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs index e7550eea..1eff1919 100644 --- a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -19,16 +19,16 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter { - public string Needle { get; init; } - public ModSearchType Type { get; init; } - public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; } + public string Needle { get; init; } + public ModSearchType Type { get; init; } + public ChangedItemIconFlag IconFlagFilter { get; init; } public bool Contains(Entry other) { if (Type != other.Type) return false; if (Type is ModSearchType.Category) - return IconFilter == other.IconFilter; + return IconFlagFilter == other.IconFlagFilter; return Needle.Contains(other.Needle); } @@ -77,7 +77,7 @@ public sealed class ModSearchStringSplitter : SearchStringSplitter leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal), ModSearchType.Category => leaf.Value.ChangedItems.Any(p - => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0), + => ((p.Value?.Icon.ToFlag() ?? ChangedItemIconFlag.Unknown) & entry.IconFlagFilter) != 0), _ => true, }; diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 2aeaaea0..256b0d79 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -6,6 +6,7 @@ using OtterGui.Services; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; +using Penumbra.GameData.Data; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -66,22 +67,22 @@ public class ChangedItemsTab( } /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, object?)> item) + private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData?)> item) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, object?)> item) + private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData?)> item) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Value.Item2); ImGui.SameLine(); drawer.DrawChangedItem(item.Key, item.Value.Item2); ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - drawer.DrawModelData(item.Value.Item2); + ChangedItemDrawer.DrawModelData(item.Value.Item2); } private void DrawModColumn(SingleArray mods) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ab47ce7c..6c36e49a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -429,7 +429,7 @@ public class SettingsTab : ITab, IUiService _config.HideChangedItemFilters = v; if (v) { - _config.Ephemeral.ChangedItemFilter = ChangedItemDrawer.AllFlags; + _config.Ephemeral.ChangedItemFilter = ChangedItemFlagExtensions.AllFlags; _config.Ephemeral.Save(); } }); diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index cb43ac06..5bd3f77c 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -10,7 +10,7 @@ namespace Penumbra.Util; public static class IdentifierExtensions { public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, - IDictionary changedItems) + IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); @@ -19,25 +19,25 @@ public static class IdentifierExtensions manip.AddChangedItems(identifier, changedItems); } - public static void RemoveMachinistOffhands(this SortedList changedItems) + public static void RemoveMachinistOffhands(this SortedList changedItems) { for (var i = 0; i < changedItems.Count; i++) { { var value = changedItems.Values[i]; - if (value is EquipItem { Type: FullEquipType.GunOff }) + if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff }) changedItems.RemoveAt(i--); } } } - public static void RemoveMachinistOffhands(this SortedList, object?)> changedItems) + public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData?)> changedItems) { for (var i = 0; i < changedItems.Count; i++) { { var value = changedItems.Values[i].Item2; - if (value is EquipItem { Type: FullEquipType.GunOff }) + if (value is IdentifiedItem { Item.Type: FullEquipType.GunOff }) changedItems.RemoveAt(i--); } } From ee086e3e7698a846cefa20c23dc09408b76caad8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 4 Aug 2024 00:57:39 +0200 Subject: [PATCH 320/865] Update GameData --- Penumbra.GameData | 2 +- Penumbra/Services/StainService.cs | 4 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ee6c6faa..1ec903d5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ee6c6faa1e4a3e96279cb6c89df96e351f112c6a +Subproject commit 1ec903d53747fc16f62139e2ed3541f224ee3403 diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 50713968..ba5c3e63 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. public int CurrentDyeChannel = 0; @@ -102,7 +102,7 @@ public class StainService : IService }; /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. - private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack { if (stmResourceHandle != null) { diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ead02874..7dae19c8 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -713,7 +713,7 @@ public class DebugTab : Window, ITab, IUiService } } - private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack + private static void DrawStainTemplatesFile(StmFile stmFile) where TDyePack : unmanaged, IDyePack { foreach (var (key, data) in stmFile.Entries) { From d90c3dd1af6e256932140ce84ddba78d4f233616 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 15:35:27 +0200 Subject: [PATCH 321/865] Update row type names. --- Penumbra.GameData | 2 +- .../ModEditWindow.Materials.ColorTable.cs | 16 ++++++++-------- .../ModEditWindow.Materials.MtrlTab.cs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 44427ad0..f2734d54 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 44427ad0149059ab5ccb4e4a2f42a1a43423e4c5 +Subproject commit f2734d543d9b2debecb8feb6d6fa928801eb2bcb diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs index 25c0e448..cb04dc0a 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs @@ -171,7 +171,7 @@ public partial class ModEditWindow } [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTable.Row row, ColorDyeTable.Row dye) + private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye) { if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, "Export this row to your clipboard.", false, true)) @@ -179,11 +179,11 @@ public partial class ModEditWindow try { - Span data = stackalloc byte[ColorTable.Row.Size + ColorDyeTable.Row.Size]; + Span data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size]; fixed (byte* ptr = data) { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTable.Row.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTable.Row.Size, &dye, ColorDyeTable.Row.Size); + MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size); + MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size); } var text = Convert.ToBase64String(data); @@ -219,15 +219,15 @@ public partial class ModEditWindow { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - if (data.Length != ColorTable.Row.Size + ColorDyeTable.Row.Size + if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size || !tab.Mtrl.HasTable) return false; fixed (byte* ptr = data) { - tab.Mtrl.Table[rowIdx] = *(ColorTable.Row*)ptr; + tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr; if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTable.Row*)(ptr + ColorTable.Row.Size); + tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size); } tab.UpdateColorTableRowPreview(rowIdx); @@ -453,7 +453,7 @@ public partial class ModEditWindow return ret; } - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) { var stain = _stainService.StainCombo.CurrentSelection.Key; if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs index 29fd7531..b95eca9d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs @@ -597,11 +597,11 @@ public partial class ModEditWindow if (!Mtrl.HasTable) return; - var row = new LegacyColorTable.Row(Mtrl.Table[rowIdx]); + var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]); if (Mtrl.HasDyeTable) { var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTable.Row(Mtrl.DyeTable[rowIdx]); + var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]); if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) row.ApplyDyeTemplate(dye, dyes); } @@ -651,7 +651,7 @@ public partial class ModEditWindow } } - private static void ApplyHighlight(ref LegacyColorTable.Row row, float time) + private static void ApplyHighlight(ref LegacyColorTableRow row, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = ColorId.InGameHighlight.Value(); From c8e859ae05ebb9d9b7f0fbce17d3223c19c88be3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 15:41:40 +0200 Subject: [PATCH 322/865] Fixups. --- .../Materials/MtrlTab.LegacyColorTable.cs | 38 +- .../Materials/MtrlTab.LivePreview.cs | 6 +- .../ModEditWindow.Materials.ColorTable.cs | 538 ------------ .../ModEditWindow.Materials.MtrlTab.cs | 783 ------------------ 4 files changed, 28 insertions(+), 1337 deletions(-) delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs delete mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index f3ec5307..a2165760 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -33,6 +33,7 @@ public partial class MtrlTab UpdateColorTableRowPreview(i); ret = true; } + ImGui.TableNextRow(); } @@ -56,6 +57,7 @@ public partial class MtrlTab UpdateColorTableRowPreview(i); ret = true; } + ImGui.TableNextRow(); } @@ -108,8 +110,10 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) - ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + } ImGui.TableNextColumn(); using var dis = ImRaii.Disabled(disabled); @@ -131,9 +135,11 @@ public partial class MtrlTab ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, b => dyeTable[rowIdx].SpecularColor = b); } + ImGui.SameLine(); ImGui.SetNextItemWidth(pctSize); - ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.SpecularMask * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].SpecularMask = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -155,7 +161,8 @@ public partial class MtrlTab ImGui.TableNextColumn(); ImGui.SetNextItemWidth(floatSize); var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Shininess * 0.025f), + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Shininess, "%.1f"u8, glossStrengthMin, HalfMaxValue, + Math.Max(0.1f, (float)row.Shininess * 0.025f), v => table[rowIdx].Shininess = v); if (dyeTable != null) @@ -197,7 +204,7 @@ public partial class MtrlTab { using var id = ImRaii.PushId(rowIdx); ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; var floatSize = LegacyColorTableFloatSize * UiHelpers.Scale; var pctSize = LegacyColorTablePercentageSize * UiHelpers.Scale; var intSize = LegacyColorTableIntegerSize * UiHelpers.Scale; @@ -213,8 +220,10 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + } ImGui.TableNextColumn(); using var dis = ImRaii.Disabled(disabled); @@ -236,6 +245,7 @@ public partial class MtrlTab ret |= CtApplyStainCheckbox("##dyeSpecular"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, b => dyeTable[rowIdx].SpecularColor = b); } + ImGui.SameLine(); ImGui.SetNextItemWidth(pctSize); ret |= CtDragScalar("##SpecularMask"u8, "Specular Strength"u8, (float)row.Scalar7 * 100.0f, "%.0f%%"u8, 0f, HalfMaxValue * 100.0f, 1.0f, @@ -260,7 +270,8 @@ public partial class MtrlTab ImGui.TableNextColumn(); ImGui.SetNextItemWidth(floatSize); var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, Math.Max(0.1f, (float)row.Scalar3 * 0.025f), + ret |= CtDragHalf("##Shininess"u8, "Gloss Strength"u8, row.Scalar3, "%.1f"u8, glossStrengthMin, HalfMaxValue, + Math.Max(0.1f, (float)row.Scalar3 * 0.025f), v => table[rowIdx].Scalar3 = v); if (dyeTable != null) @@ -307,7 +318,7 @@ public partial class MtrlTab return ret; } - private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTable.Row dye, float floatSize) + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize) { var stain = _stainService.StainCombo1.CurrentSelection.Key; if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) @@ -326,7 +337,7 @@ public partial class MtrlTab return ret; } - private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTable.Row dye, float floatSize) + private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) { var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) @@ -337,10 +348,11 @@ public partial class MtrlTab var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Apply the selected dye to this row.", disabled, true); - ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ - _stainService.StainCombo1.CurrentSelection.Key, - _stainService.StainCombo2.CurrentSelection.Key, - ], rowIdx); + ret = ret + && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ + _stainService.StainCombo1.CurrentSelection.Key, + _stainService.StainCombo2.CurrentSelection.Key, + ], rowIdx); ImGui.SameLine(); DrawLegacyDyePreview(values, floatSize); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index bb346534..3482e581 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -191,7 +191,7 @@ public partial class MtrlTab var row = Mtrl.Table switch { - LegacyColorTable legacyTable => new ColorTable.Row(legacyTable[rowIdx]), + LegacyColorTable legacyTable => new ColorTableRow(legacyTable[rowIdx]), ColorTable table => table[rowIdx], _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), }; @@ -199,7 +199,7 @@ public partial class MtrlTab { var dyeRow = Mtrl.DyeTable switch { - LegacyColorDyeTable legacyDyeTable => new ColorDyeTable.Row(legacyDyeTable[rowIdx]), + LegacyColorDyeTable legacyDyeTable => new ColorDyeTableRow(legacyDyeTable[rowIdx]), ColorDyeTable dyeTable => dyeTable[rowIdx], _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), }; @@ -258,7 +258,7 @@ public partial class MtrlTab } } - private static void ApplyHighlight(ref ColorTable.Row row, ColorId colorId, float time) + private static void ApplyHighlight(ref ColorTableRow row, ColorId colorId, float time) { var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; var baseColor = colorId.Value(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs deleted file mode 100644 index cb04dc0a..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.ColorTable.cs +++ /dev/null @@ -1,538 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.String.Functions; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private static readonly float HalfMinValue = (float)Half.MinValue; - private static readonly float HalfMaxValue = (float)Half.MaxValue; - private static readonly float HalfEpsilon = (float)Half.Epsilon; - - private bool DrawMaterialColorTableChange(MtrlTab tab, bool disabled) - { - if (!tab.SamplerIds.Contains(ShpkFile.TableSamplerId) || !tab.Mtrl.HasTable) - return false; - - ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) - return false; - - ColorTableCopyAllClipboardButton(tab.Mtrl); - ImGui.SameLine(); - var ret = ColorTablePasteAllClipboardButton(tab, disabled); - if (!disabled) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= ColorTableDyeableCheckbox(tab); - } - - var hasDyeTable = tab.Mtrl.HasDyeTable; - if (hasDyeTable) - { - ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); - ImGui.SameLine(); - ret |= DrawPreviewDye(tab, disabled); - } - - using var table = ImRaii.Table("##ColorTable", hasDyeTable ? 11 : 9, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV); - if (!table) - return false; - - ImGui.TableNextColumn(); - ImGui.TableHeader(string.Empty); - ImGui.TableNextColumn(); - ImGui.TableHeader("Row"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Diffuse"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Specular"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Emissive"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Gloss"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Tile"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Repeat"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Skew"); - if (hasDyeTable) - { - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye"); - ImGui.TableNextColumn(); - ImGui.TableHeader("Dye Preview"); - } - - for (var i = 0; i < ColorTable.NumRows; ++i) - { - ret |= DrawColorTableRow(tab, i, disabled); - ImGui.TableNextRow(); - } - - return ret; - } - - - private static void ColorTableCopyAllClipboardButton(MtrlFile file) - { - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) - return; - - try - { - var data1 = file.Table.AsBytes(); - var data2 = file.HasDyeTable ? file.DyeTable.AsBytes() : ReadOnlySpan.Empty; - var array = new byte[data1.Length + data2.Length]; - data1.TryCopyTo(array); - data2.TryCopyTo(array.AsSpan(data1.Length)); - var text = Convert.ToBase64String(array); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private bool DrawPreviewDye(MtrlTab tab, bool disabled) - { - var (dyeId, (name, dyeColor, gloss)) = _stainService.StainCombo.CurrentSelection; - var tt = dyeId == 0 - ? "Select a preview dye first." - : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; - if (ImGuiUtil.DrawDisabledButton("Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0)) - { - var ret = false; - if (tab.Mtrl.HasDyeTable) - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - ret |= tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, i, dyeId, 0); - - tab.UpdateColorTablePreview(); - - return ret; - } - - ImGui.SameLine(); - var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; - if (_stainService.StainCombo.Draw(label, dyeColor, string.Empty, true, gloss)) - tab.UpdateColorTablePreview(); - return false; - } - - private static unsafe bool ColorTablePasteAllClipboardButton(MtrlTab tab, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton("Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2(200, 0), string.Empty, disabled) - || !tab.Mtrl.HasTable) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length < Marshal.SizeOf()) - return false; - - ref var rows = ref tab.Mtrl.Table; - fixed (void* ptr = data, output = &rows) - { - MemoryUtility.MemCpyUnchecked(output, ptr, Marshal.SizeOf()); - if (data.Length >= Marshal.SizeOf() + Marshal.SizeOf() - && tab.Mtrl.HasDyeTable) - { - ref var dyeRows = ref tab.Mtrl.DyeTable; - fixed (void* output2 = &dyeRows) - { - MemoryUtility.MemCpyUnchecked(output2, (byte*)ptr + Marshal.SizeOf(), - Marshal.SizeOf()); - } - } - } - - tab.UpdateColorTablePreview(); - - return true; - } - catch - { - return false; - } - } - - [SkipLocalsInit] - private static unsafe void ColorTableCopyClipboardButton(ColorTableRow row, ColorDyeTableRow dye) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Export this row to your clipboard.", false, true)) - return; - - try - { - Span data = stackalloc byte[ColorTableRow.Size + ColorDyeTableRow.Size]; - fixed (byte* ptr = data) - { - MemoryUtility.MemCpyUnchecked(ptr, &row, ColorTableRow.Size); - MemoryUtility.MemCpyUnchecked(ptr + ColorTableRow.Size, &dye, ColorDyeTableRow.Size); - } - - var text = Convert.ToBase64String(data); - ImGui.SetClipboardText(text); - } - catch - { - // ignored - } - } - - private static bool ColorTableDyeableCheckbox(MtrlTab tab) - { - var dyeable = tab.Mtrl.HasDyeTable; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); - - if (ret) - { - tab.Mtrl.HasDyeTable = dyeable; - tab.UpdateColorTablePreview(); - } - - return ret; - } - - private static unsafe bool ColorTablePasteFromClipboardButton(MtrlTab tab, int rowIdx, bool disabled) - { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Import an exported row from your clipboard onto this row.", disabled, true)) - return false; - - try - { - var text = ImGui.GetClipboardText(); - var data = Convert.FromBase64String(text); - if (data.Length != ColorTableRow.Size + ColorDyeTableRow.Size - || !tab.Mtrl.HasTable) - return false; - - fixed (byte* ptr = data) - { - tab.Mtrl.Table[rowIdx] = *(ColorTableRow*)ptr; - if (tab.Mtrl.HasDyeTable) - tab.Mtrl.DyeTable[rowIdx] = *(ColorDyeTableRow*)(ptr + ColorTableRow.Size); - } - - tab.UpdateColorTableRowPreview(rowIdx); - - return true; - } - catch - { - return false; - } - } - - private static void ColorTableHighlightButton(MtrlTab tab, int rowIdx, bool disabled) - { - ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Crosshairs.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Highlight this row on your character, if possible.", disabled || tab.ColorTablePreviewers.Count == 0, true); - - if (ImGui.IsItemHovered()) - tab.HighlightColorTableRow(rowIdx); - else if (tab.HighlightedColorTableRow == rowIdx) - tab.CancelColorTableHighlight(); - } - - private bool DrawColorTableRow(MtrlTab tab, int rowIdx, bool disabled) - { - static bool FixFloat(ref float val, float current) - { - val = (float)(Half)val; - return val != current; - } - - using var id = ImRaii.PushId(rowIdx); - ref var row = ref tab.Mtrl.Table[rowIdx]; - var hasDye = tab.Mtrl.HasDyeTable; - ref var dye = ref tab.Mtrl.DyeTable[rowIdx]; - var floatSize = 70 * UiHelpers.Scale; - var intSize = 45 * UiHelpers.Scale; - ImGui.TableNextColumn(); - ColorTableCopyClipboardButton(row, dye); - ImGui.SameLine(); - var ret = ColorTablePasteFromClipboardButton(tab, rowIdx, disabled); - ImGui.SameLine(); - ColorTableHighlightButton(tab, rowIdx, disabled); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"#{rowIdx + 1:D2}"); - - ImGui.TableNextColumn(); - using var dis = ImRaii.Disabled(disabled); - ret |= ColorPicker("##Diffuse", "Diffuse Color", row.Diffuse, c => - { - tab.Mtrl.Table[rowIdx].Diffuse = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, - b => - { - tab.Mtrl.DyeTable[rowIdx].Diffuse = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Specular", "Specular Color", row.Specular, c => - { - tab.Mtrl.Table[rowIdx].Specular = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - ImGui.SameLine(); - var tmpFloat = row.SpecularStrength; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SpecularStrength", ref tmpFloat, 0.01f, 0f, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.SpecularStrength)) - { - row.SpecularStrength = tmpFloat; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, - b => - { - tab.Mtrl.DyeTable[rowIdx].Specular = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, - b => - { - tab.Mtrl.DyeTable[rowIdx].SpecularStrength = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - ret |= ColorPicker("##Emissive", "Emissive Color", row.Emissive, c => - { - tab.Mtrl.Table[rowIdx].Emissive = c; - tab.UpdateColorTableRowPreview(rowIdx); - }); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, - b => - { - tab.Mtrl.DyeTable[rowIdx].Emissive = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - tmpFloat = row.GlossStrength; - ImGui.SetNextItemWidth(floatSize); - var glossStrengthMin = ImGui.GetIO().KeyCtrl ? 0.0f : HalfEpsilon; - if (ImGui.DragFloat("##GlossStrength", ref tmpFloat, Math.Max(0.1f, tmpFloat * 0.025f), glossStrengthMin, HalfMaxValue, "%.1f") - && FixFloat(ref tmpFloat, row.GlossStrength)) - { - row.GlossStrength = Math.Max(tmpFloat, glossStrengthMin); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled); - if (hasDye) - { - ImGui.SameLine(); - ret |= ImGuiUtil.Checkbox("##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, - b => - { - tab.Mtrl.DyeTable[rowIdx].Gloss = b; - tab.UpdateColorTableRowPreview(rowIdx); - }, ImGuiHoveredFlags.AllowWhenDisabled); - } - - ImGui.TableNextColumn(); - int tmpInt = row.TileSet; - ImGui.SetNextItemWidth(intSize); - if (ImGui.DragInt("##TileSet", ref tmpInt, 0.25f, 0, 63) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue) - { - row.TileSet = (ushort)Math.Clamp(tmpInt, 0, 63); - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Tile Set", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialRepeat.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.X)) - { - row.MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat X", ImGuiHoveredFlags.AllowWhenDisabled); - ImGui.SameLine(); - tmpFloat = row.MaterialRepeat.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##RepeatY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") - && FixFloat(ref tmpFloat, row.MaterialRepeat.Y)) - { - row.MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - tmpFloat = row.MaterialSkew.X; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewX", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.X)) - { - row.MaterialSkew = row.MaterialSkew with { X = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew X", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.SameLine(); - tmpFloat = row.MaterialSkew.Y; - ImGui.SetNextItemWidth(floatSize); - if (ImGui.DragFloat("##SkewY", ref tmpFloat, 0.1f, HalfMinValue, HalfMaxValue, "%.2f") && FixFloat(ref tmpFloat, row.MaterialSkew.Y)) - { - row.MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Skew Y", ImGuiHoveredFlags.AllowWhenDisabled); - - if (hasDye) - { - ImGui.TableNextColumn(); - if (_stainService.TemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) - { - dye.Template = _stainService.TemplateCombo.CurrentSelection; - ret = true; - tab.UpdateColorTableRowPreview(rowIdx); - } - - ImGuiUtil.HoverTooltip("Dye Template", ImGuiHoveredFlags.AllowWhenDisabled); - - ImGui.TableNextColumn(); - ret |= DrawDyePreview(tab, rowIdx, disabled, dye, floatSize); - } - - - return ret; - } - - private bool DrawDyePreview(MtrlTab tab, int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) - { - var stain = _stainService.StainCombo.CurrentSelection.Key; - if (stain == 0 || !_stainService.StmFile.Entries.TryGetValue(dye.Template, out var entry)) - return false; - - var values = entry[(int)stain]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2); - - var ret = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Apply the selected dye to this row.", disabled, true); - - ret = ret && tab.Mtrl.ApplyDyeTemplate(_stainService.StmFile, rowIdx, stain, 0); - if (ret) - tab.UpdateColorTableRowPreview(rowIdx); - - ImGui.SameLine(); - ColorPicker("##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D"); - ImGui.SameLine(); - ColorPicker("##specularPreview", string.Empty, values.Specular, _ => { }, "S"); - ImGui.SameLine(); - ColorPicker("##emissivePreview", string.Empty, values.Emissive, _ => { }, "E"); - ImGui.SameLine(); - using var dis = ImRaii.Disabled(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##gloss", ref values.Gloss, 0, values.Gloss, values.Gloss, "%.1f G"); - ImGui.SameLine(); - ImGui.SetNextItemWidth(floatSize); - ImGui.DragFloat("##specularStrength", ref values.SpecularPower, 0, values.SpecularPower, values.SpecularPower, "%.2f S"); - - return ret; - } - - private static bool ColorPicker(string label, string tooltip, Vector3 input, Action setter, string letter = "") - { - var ret = false; - var inputSqrt = PseudoSqrtRgb(input); - var tmp = inputSqrt; - if (ImGui.ColorEdit3(label, ref tmp, - ImGuiColorEditFlags.NoInputs - | ImGuiColorEditFlags.DisplayRGB - | ImGuiColorEditFlags.InputRGB - | ImGuiColorEditFlags.NoTooltip - | ImGuiColorEditFlags.HDR) - && tmp != inputSqrt) - { - setter(PseudoSquareRgb(tmp)); - ret = true; - } - - if (letter.Length > 0 && ImGui.IsItemVisible()) - { - var textSize = ImGui.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; - var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; - ImGui.GetWindowDrawList().AddText(center, textColor, letter); - } - - ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled); - - return ret; - } - - // Functions to deal with squared RGB values without making negatives useless. - - private static float PseudoSquareRgb(float x) - => x < 0.0f ? -(x * x) : x * x; - - private static Vector3 PseudoSquareRgb(Vector3 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z)); - - private static Vector4 PseudoSquareRgb(Vector4 vec) - => new(PseudoSquareRgb(vec.X), PseudoSquareRgb(vec.Y), PseudoSquareRgb(vec.Z), vec.W); - - private static float PseudoSqrtRgb(float x) - => x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x); - - internal static Vector3 PseudoSqrtRgb(Vector3 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z)); - - private static Vector4 PseudoSqrtRgb(Vector4 vec) - => new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z), vec.W); -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs deleted file mode 100644 index b95eca9d..00000000 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.MtrlTab.cs +++ /dev/null @@ -1,783 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Newtonsoft.Json.Linq; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.Objects; -using Penumbra.Interop.MaterialPreview; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; -using static Penumbra.GameData.Files.ShpkFile; - -namespace Penumbra.UI.AdvancedWindow; - -public partial class ModEditWindow -{ - private sealed class MtrlTab : IWritable, IDisposable - { - private const int ShpkPrefixLength = 16; - - private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - - private readonly ModEditWindow _edit; - public readonly MtrlFile Mtrl; - public readonly string FilePath; - public readonly bool Writable; - - private string[]? _shpkNames; - - public string ShaderHeader = "Shader###Shader"; - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public string LoadedShpkDevkitPathName = string.Empty; - public string ShaderComment = string.Empty; - public ShpkFile? AssociatedShpk; - public JObject? AssociatedShpkDevkit; - - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; - - // Shader Key State - public readonly - List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); - - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public string VertexShadersString = "Vertex Shaders: ???"; - public string PixelShadersString = "Pixel Shaders: ???"; - - // Textures & Samplers - public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); - - public readonly HashSet UnfoldedTextures = new(4); - public readonly HashSet SamplerIds = new(16); - public float TextureLabelWidth; - - // Material Constants - public readonly - List<(string Header, List<(string Label, int ConstantIndex, Range Slice, string Description, bool MonoFont, IConstantEditor Editor)> - Constants)> Constants = new(16); - - // Live-Previewers - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTableRow = -1; - public readonly Stopwatch HighlightTime = new(); - - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) - { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); - if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) - return FullPath.Empty; - - return _edit.FindBestMatch(defaultGamePath); - } - - public string[] GetShpkNames() - { - if (null != _shpkNames) - return _shpkNames; - - var names = new HashSet(StandardShaderPackages); - names.UnionWith(_edit.FindPathsStartingWith(ShpkPrefix).Select(path => path.ToString()[ShpkPrefixLength..])); - - _shpkNames = names.ToArray(); - Array.Sort(_shpkNames); - - return _shpkNames; - } - - public void LoadShpk(FullPath path) - { - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - - try - { - LoadedShpkPath = path; - var data = LoadedShpkPath.IsRooted - ? File.ReadAllBytes(LoadedShpkPath.FullName) - : _edit._gameData.GetFile(LoadedShpkPath.InternalName.ToString())?.Data; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); - } - catch (Exception e) - { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); - } - - if (LoadedShpkPath.InternalName.IsEmpty) - { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; - } - else - { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); - } - - UpdateShaderKeys(); - Update(); - } - - private JObject? TryLoadShpkDevkit(string shpkBaseName, out string devkitPathName) - { - try - { - if (!Utf8GamePath.FromString("penumbra/shpk_devkit/" + shpkBaseName + ".json", out var devkitPath)) - throw new Exception("Could not assemble ShPk dev-kit path."); - - var devkitFullPath = _edit.FindBestMatch(devkitPath); - if (!devkitFullPath.IsRooted) - throw new Exception("Could not resolve ShPk dev-kit path."); - - devkitPathName = devkitFullPath.FullName; - return JObject.Parse(File.ReadAllText(devkitFullPath.FullName)); - } - catch - { - devkitPathName = string.Empty; - return null; - } - } - - private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); - - private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class - { - if (devkit == null) - return null; - - try - { - var data = devkit[category]; - if (id.HasValue) - data = data?[id.Value.ToString()]; - - if (mayVary && (data as JObject)?["Vary"] != null) - { - var selector = BuildSelector(data!["Vary"]! - .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); - var index = (int)data["Selectors"]![selector.ToString()]!; - data = data["Items"]![index]; - } - - return data?.ToObject(typeof(T)) as T; - } - catch (Exception e) - { - // Some element in the JSON was undefined or invalid (wrong type, key that doesn't exist in the ShPk, index out of range, …) - Penumbra.Log.Error($"Error while traversing the ShPk dev-kit file at {devkitPathName}: {e}"); - return null; - } - } - - private void UpdateShaderKeys() - { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) - { - var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var valueSet = new HashSet(key.Values); - if (dkData != null) - valueSet.UnionWith(dkData.Values.Keys); - - var mtrlKeyIndex = Mtrl.FindOrAddShaderKey(key.Id, key.DefaultValue); - var values = valueSet.Select(value => - { - if (dkData != null && dkData.Values.TryGetValue(value, out var dkValue)) - return (dkValue.Label.Length > 0 ? dkValue.Label : $"0x{value:X8}", value, dkValue.Description); - - return ($"0x{value:X8}", value, string.Empty); - }).ToArray(); - Array.Sort(values, (x, y) => - { - if (x.Value == key.DefaultValue) - return -1; - if (y.Value == key.DefaultValue) - return 1; - - return string.Compare(x.Label, y.Label, StringComparison.Ordinal); - }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : $"0x{key.Id:X8}", mtrlKeyIndex, dkData?.Description ?? string.Empty, - !hasDkLabel, values)); - } - else - foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) - ShaderKeys.Add(($"0x{key.Category:X8}", index, string.Empty, true, Array.Empty<(string, uint, string)>())); - } - - private void UpdateShaders() - { - VertexShaders.Clear(); - PixelShaders.Clear(); - if (AssociatedShpk == null) - { - ShadersKnown = false; - } - else - { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); - var materialKeySelector = - BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); - foreach (var systemKeySelector in systemKeySelectors) - { - foreach (var sceneKeySelector in sceneKeySelectors) - { - foreach (var subViewKeySelector in subViewKeySelectors) - { - var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); - if (node.HasValue) - foreach (var pass in node.Value.Passes) - { - VertexShaders.Add((int)pass.VertexShader); - PixelShaders.Add((int)pass.PixelShader); - } - else - ShadersKnown = false; - } - } - } - } - - var vertexShaders = VertexShaders.OrderBy(i => i).Select(i => $"#{i}"); - var pixelShaders = PixelShaders.OrderBy(i => i).Select(i => $"#{i}"); - - VertexShadersString = $"Vertex Shaders: {string.Join(", ", ShadersKnown ? vertexShaders : vertexShaders.Append("???"))}"; - PixelShadersString = $"Pixel Shaders: {string.Join(", ", ShadersKnown ? pixelShaders : pixelShaders.Append("???"))}"; - - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; - } - - private void UpdateTextures() - { - Textures.Clear(); - SamplerIds.Clear(); - if (AssociatedShpk == null) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - - foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) - Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); - } - else - { - foreach (var index in VertexShaders) - SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in PixelShaders) - SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!ShadersKnown) - { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.HasTable) - SamplerIds.Add(TableSamplerId); - } - - foreach (var samplerId in SamplerIds) - { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) - continue; - - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); - var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - - var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, - dkData?.Description ?? string.Empty, !hasDkLabel)); - } - - if (SamplerIds.Contains(TableSamplerId)) - Mtrl.HasTable = true; - } - - Textures.Sort((x, y) => string.CompareOrdinal(x.Label, y.Label)); - - TextureLabelWidth = 50f * UiHelpers.Scale; - - float helpWidth; - using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) - { - helpWidth = ImGui.GetStyle().ItemSpacing.X + ImGui.CalcTextSize(FontAwesomeIcon.InfoCircle.ToIconString()).X; - } - - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (!monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) - { - foreach (var (label, _, _, description, monoFont) in Textures) - { - if (monoFont) - TextureLabelWidth = Math.Max(TextureLabelWidth, - ImGui.CalcTextSize(label).X + (description.Length > 0 ? helpWidth : 0.0f)); - } - } - - TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; - } - - private void UpdateConstants() - { - static List FindOrAddGroup(List<(string, List)> groups, string name) - { - foreach (var (groupName, group) in groups) - { - if (string.Equals(name, groupName, StringComparison.Ordinal)) - return group; - } - - var newGroup = new List(16); - groups.Add((name, newGroup)); - return newGroup; - } - - Constants.Clear(); - if (AssociatedShpk == null) - { - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (constant, index) in Mtrl.ShaderPackage.Constants.WithIndex()) - { - var values = Mtrl.GetConstantValues(constant); - for (var i = 0; i < values.Length; i += 4) - { - fcGroup.Add(($"0x{constant.Id:X8}", index, i..Math.Min(i + 4, values.Length), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - else - { - var prefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? string.Empty; - foreach (var shpkConstant in AssociatedShpk.MaterialParams) - { - if ((shpkConstant.ByteSize & 0x3) != 0) - continue; - - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, shpkConstant.ByteSize >> 2, out var constantIndex); - var values = Mtrl.GetConstantValues(constant); - var handledElements = new IndexSet(values.Length, false); - - var dkData = TryGetShpkDevkitData("Constants", shpkConstant.Id, true); - if (dkData != null) - foreach (var dkConstant in dkData) - { - var offset = (int)dkConstant.Offset; - var length = values.Length - offset; - if (dkConstant.Length.HasValue) - length = Math.Min(length, (int)dkConstant.Length.Value); - if (length <= 0) - continue; - - var editor = dkConstant.CreateEditor(); - if (editor != null) - FindOrAddGroup(Constants, dkConstant.Group.Length > 0 ? dkConstant.Group : "Further Constants") - .Add((dkConstant.Label, constantIndex, offset..(offset + length), dkConstant.Description, false, editor)); - handledElements.AddRange(offset, length); - } - - var fcGroup = FindOrAddGroup(Constants, "Further Constants"); - foreach (var (start, end) in handledElements.Ranges(complement:true)) - { - if ((shpkConstant.ByteOffset & 0x3) == 0) - { - var offset = shpkConstant.ByteOffset >> 2; - for (int i = (start & ~0x3) - (offset & 0x3), j = offset >> 2; i < end; i += 4, ++j) - { - var rangeStart = Math.Max(i, start); - var rangeEnd = Math.Min(i + 4, end); - if (rangeEnd > rangeStart) - fcGroup.Add(( - $"{prefix}[{j:D2}]{VectorSwizzle((offset + rangeStart) & 0x3, (offset + rangeEnd - 1) & 0x3)} (0x{shpkConstant.Id:X8})", - constantIndex, rangeStart..rangeEnd, string.Empty, true, FloatConstantEditor.Default)); - } - } - else - { - for (var i = start; i < end; i += 4) - { - fcGroup.Add(($"0x{shpkConstant.Id:X8}", constantIndex, i..Math.Min(i + 4, end), string.Empty, true, - FloatConstantEditor.Default)); - } - } - } - } - } - - Constants.RemoveAll(group => group.Constants.Count == 0); - Constants.Sort((x, y) => - { - if (string.Equals(x.Header, "Further Constants", StringComparison.Ordinal)) - return 1; - if (string.Equals(y.Header, "Further Constants", StringComparison.Ordinal)) - return -1; - - return string.Compare(x.Header, y.Header, StringComparison.Ordinal); - }); - // HACK the Replace makes w appear after xyz, for the cbuffer-location-based naming scheme - foreach (var (_, group) in Constants) - { - group.Sort((x, y) => string.CompareOrdinal( - x.MonoFont ? x.Label.Replace("].w", "].{") : x.Label, - y.MonoFont ? y.Label.Replace("].w", "].{") : y.Label)); - } - } - - public unsafe void BindToMaterialInstances() - { - UnbindFromMaterialInstances(); - - var instances = MaterialInfo.FindMaterials(_edit._resourceTreeFactory.GetLocalPlayerRelatedCharacters().Select(ch => ch.Address), - FilePath); - - var foundMaterials = new HashSet(); - foreach (var materialInfo in instances) - { - var material = materialInfo.GetDrawObjectMaterial(_edit._objects); - if (foundMaterials.Contains((nint)material)) - continue; - - try - { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_edit._objects, materialInfo)); - foundMaterials.Add((nint)material); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateMaterialPreview(); - - if (!Mtrl.HasTable) - return; - - foreach (var materialInfo in instances) - { - try - { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_edit._objects, _edit._framework, materialInfo)); - } - catch (InvalidOperationException) - { - // Carry on without that previewer. - } - } - - UpdateColorTablePreview(); - } - - private void UnbindFromMaterialInstances() - { - foreach (var previewer in MaterialPreviewers) - previewer.Dispose(); - MaterialPreviewers.Clear(); - - foreach (var previewer in ColorTablePreviewers) - previewer.Dispose(); - ColorTablePreviewers.Clear(); - } - - private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) - { - for (var i = MaterialPreviewers.Count; i-- > 0;) - { - var previewer = MaterialPreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); - } - - for (var i = ColorTablePreviewers.Count; i-- > 0;) - { - var previewer = ColorTablePreviewers[i]; - if (previewer.DrawObject != characterBase) - continue; - - previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); - } - } - - public void SetShaderPackageFlags(uint shPkFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetShaderPackageFlags(shPkFlags); - } - - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetMaterialParameter(parameterCrc, offset, value); - } - - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) - { - foreach (var previewer in MaterialPreviewers) - previewer.SetSamplerFlags(samplerCrc, samplerFlags); - } - - private void UpdateMaterialPreview() - { - SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); - foreach (var constant in Mtrl.ShaderPackage.Constants) - { - var values = Mtrl.GetConstantValues(constant); - if (values != null) - SetMaterialParameter(constant.Id, 0, values); - } - - foreach (var sampler in Mtrl.ShaderPackage.Samplers) - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - public void HighlightColorTableRow(int rowIdx) - { - var oldRowIdx = HighlightedColorTableRow; - - if (HighlightedColorTableRow != rowIdx) - { - HighlightedColorTableRow = rowIdx; - HighlightTime.Restart(); - } - - if (oldRowIdx >= 0) - UpdateColorTableRowPreview(oldRowIdx); - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void CancelColorTableHighlight() - { - var rowIdx = HighlightedColorTableRow; - - HighlightedColorTableRow = -1; - HighlightTime.Reset(); - - if (rowIdx >= 0) - UpdateColorTableRowPreview(rowIdx); - } - - public void UpdateColorTableRowPreview(int rowIdx) - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var row = new LegacyColorTableRow(Mtrl.Table[rowIdx]); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var dye = new LegacyColorDyeTableRow(Mtrl.DyeTable[rowIdx]); - if (stm.TryGetValue(dye.Template, _edit._stainService.StainCombo.CurrentSelection.Key, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - - if (HighlightedColorTableRow == rowIdx) - ApplyHighlight(ref row, (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - row.AsHalves().CopyTo(previewer.ColorTable.AsSpan() - .Slice(LiveColorTablePreviewer.TextureWidth * 4 * rowIdx, LiveColorTablePreviewer.TextureWidth * 4)); - previewer.ScheduleUpdate(); - } - } - - public void UpdateColorTablePreview() - { - if (ColorTablePreviewers.Count == 0) - return; - - if (!Mtrl.HasTable) - return; - - var rows = new LegacyColorTable(Mtrl.Table); - var dyeRows = new LegacyColorDyeTable(Mtrl.DyeTable); - if (Mtrl.HasDyeTable) - { - var stm = _edit._stainService.StmFile; - var stainId = (StainId)_edit._stainService.StainCombo.CurrentSelection.Key; - for (var i = 0; i < LegacyColorTable.NumUsedRows; ++i) - { - ref var row = ref rows[i]; - var dye = dyeRows[i]; - if (stm.TryGetValue(dye.Template, stainId, out var dyes)) - row.ApplyDyeTemplate(dye, dyes); - } - } - - if (HighlightedColorTableRow >= 0) - ApplyHighlight(ref rows[HighlightedColorTableRow], (float)HighlightTime.Elapsed.TotalSeconds); - - foreach (var previewer in ColorTablePreviewers) - { - // TODO: Dawntrail - rows.AsHalves().CopyTo(previewer.ColorTable); - previewer.ScheduleUpdate(); - } - } - - private static void ApplyHighlight(ref LegacyColorTableRow row, float time) - { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = ColorId.InGameHighlight.Value(); - var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); - - row.Diffuse = Vector3.Zero; - row.Specular = Vector3.Zero; - row.Emissive = color * color; - } - - public void Update() - { - UpdateShaders(); - UpdateTextures(); - UpdateConstants(); - } - - public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable) - { - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); - LoadShpk(FindAssociatedShpk(out _, out _)); - if (writable) - { - _edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab); - BindToMaterialInstances(); - } - } - - public unsafe void Dispose() - { - UnbindFromMaterialInstances(); - if (Writable) - _edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances); - } - - // TODO Readd ShadersKnown - public bool Valid - => (true || ShadersKnown) && Mtrl.Valid; - - public byte[] Write() - { - var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); - - return output.Write(); - } - - private sealed class DevkitShaderKeyValue - { - public string Label = string.Empty; - public string Description = string.Empty; - } - - private sealed class DevkitShaderKey - { - public string Label = string.Empty; - public string Description = string.Empty; - public Dictionary Values = new(); - } - - private sealed class DevkitSampler - { - public string Label = string.Empty; - public string Description = string.Empty; - public string DefaultTexture = string.Empty; - } - - private enum DevkitConstantType - { - Hidden = -1, - Float = 0, - Integer = 1, - Color = 2, - Enum = 3, - } - - private sealed class DevkitConstantValue - { - public string Label = string.Empty; - public string Description = string.Empty; - public float Value = 0; - } - - private sealed class DevkitConstant - { - public uint Offset = 0; - public uint? Length = null; - public string Group = string.Empty; - public string Label = string.Empty; - public string Description = string.Empty; - public DevkitConstantType Type = DevkitConstantType.Float; - - public float? Minimum = null; - public float? Maximum = null; - public float? Speed = null; - public float RelativeSpeed = 0.0f; - public float Factor = 1.0f; - public float Bias = 0.0f; - public byte Precision = 3; - public string Unit = string.Empty; - - public bool SquaredRgb = false; - public bool Clamped = false; - - public DevkitConstantValue[] Values = Array.Empty(); - - public IConstantEditor? CreateEditor() - => Type switch - { - DevkitConstantType.Hidden => null, - DevkitConstantType.Float => new FloatConstantEditor(Minimum, Maximum, Speed ?? 0.1f, RelativeSpeed, Factor, Bias, Precision, - Unit), - DevkitConstantType.Integer => new IntConstantEditor(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, - Factor, Bias, Unit), - DevkitConstantType.Color => new ColorConstantEditor(SquaredRgb, Clamped), - DevkitConstantType.Enum => new EnumConstantEditor(Array.ConvertAll(Values, - value => (value.Label, value.Value, value.Description))), - _ => FloatConstantEditor.Default, - }; - - private static int? ToInteger(float? value) - => value.HasValue ? (int)Math.Clamp(MathF.Round(value.Value), int.MinValue, int.MaxValue) : null; - } - } -} From f3ab1ddbb48f8e1bab94c59d7628b83ed9d29ba8 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 4 Aug 2024 22:27:34 +0200 Subject: [PATCH 323/865] Add game data file status to support info --- Penumbra/Penumbra.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index dbe06803..557e011c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -46,6 +46,7 @@ public class Penumbra : IDalamudPlugin private readonly CharacterUtility _characterUtility; private readonly RedrawService _redrawService; private readonly CommunicatorService _communicatorService; + private readonly IDataManager _gameData; private PenumbraWindowSystem? _windowSystem; private bool _disposed; @@ -78,6 +79,7 @@ public class Penumbra : IDalamudPlugin _tempCollections = _services.GetService(); _redrawService = _services.GetService(); _communicatorService = _services.GetService(); + _gameData = _services.GetService(); _services.GetService(); // Initialize because not required anywhere else. _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); @@ -217,6 +219,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); + sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); From f8b034c42d14c038ed79183a37bee8262b9c8a7b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 22:48:15 +0200 Subject: [PATCH 324/865] Auto-formatting, generous application of ImUtf8, minor cleanups. --- .../Materials/MtrlTab.ColorTable.cs | 246 +++++++++++------- .../Materials/MtrlTab.CommonColorTable.cs | 142 ++++++---- .../Materials/MtrlTab.Constants.cs | 29 ++- .../Materials/MtrlTab.Devkit.cs | 50 ++-- .../Materials/MtrlTab.LegacyColorTable.cs | 6 +- .../Materials/MtrlTab.LivePreview.cs | 129 ++++----- .../Materials/MtrlTab.ShaderPackage.cs | 218 ++++++++-------- .../Materials/MtrlTab.Textures.cs | 29 ++- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 40 +-- .../Materials/MtrlTabFactory.cs | 13 +- .../AdvancedWindow/ModEditWindow.Materials.cs | 21 +- .../ModEditWindow.ShaderPackages.cs | 179 +++++++------ .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 62 ++--- 13 files changed, 647 insertions(+), 517 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index dc87ec41..352681bb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -13,7 +13,7 @@ public partial class MtrlTab { private const float ColorTableScalarSize = 65.0f; - private int _colorTableSelectedPair = 0; + private int _colorTableSelectedPair; private bool DrawColorTable(ColorTable table, ColorDyeTable? dyeTable, bool disabled) { @@ -23,25 +23,27 @@ public partial class MtrlTab private void DrawColorTablePairSelector(ColorTable table, bool disabled) { + var style = ImGui.GetStyle(); + var itemSpacing = style.ItemSpacing.X; + var itemInnerSpacing = style.ItemInnerSpacing.X; + var framePadding = style.FramePadding; + var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; + var frameHeight = ImGui.GetFrameHeight(); + var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var style = ImGui.GetStyle(); - var itemSpacing = style.ItemSpacing.X; - var itemInnerSpacing = style.ItemInnerSpacing.X; - var framePadding = style.FramePadding; - var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; - var frameHeight = ImGui.GetFrameHeight(); - var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; - var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; - var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) { for (var j = 0; j < 8; ++j) { var pairIndex = i + j; - using (var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) + using (ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), pairIndex == _colorTableSelectedPair)) { - if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) + if (ImUtf8.Button($"#{pairIndex + 1}".PadLeft(3 + spacePadding), + new Vector2(buttonWidth, ImGui.GetFrameHeightWithSpacing() + frameHeight))) _colorTableSelectedPair = pairIndex; } @@ -79,12 +81,10 @@ public partial class MtrlTab private bool DrawColorTablePairEditor(ColorTable table, ColorDyeTable? dyeTable, bool disabled) { - var retA = false; - var retB = false; - ref var rowA = ref table[_colorTableSelectedPair << 1]; - ref var rowB = ref table[(_colorTableSelectedPair << 1) | 1]; - var dyeA = dyeTable != null ? dyeTable[_colorTableSelectedPair << 1] : default; - var dyeB = dyeTable != null ? dyeTable[(_colorTableSelectedPair << 1) | 1] : default; + var retA = false; + var retB = false; + var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; + var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); @@ -108,68 +108,96 @@ public partial class MtrlTab using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("ColorsA"u8)) + using (ImUtf8.PushId("ColorsA"u8)) + { retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("ColorsB"u8)) + using (ImUtf8.PushId("ColorsB"u8)) + { retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Physical Parameters"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("PbrA"u8)) + using (ImUtf8.PushId("PbrA"u8)) + { retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("PbrB"u8)) + using (ImUtf8.PushId("PbrB"u8)) + { retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Sheen Layer Parameters"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("SheenA"u8)) + using (ImUtf8.PushId("SheenA"u8)) + { retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("SheenB"u8)) + using (ImUtf8.PushId("SheenB"u8)) + { retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Pair Blending"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("BlendingA"u8)) + using (ImUtf8.PushId("BlendingA"u8)) + { retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("BlendingB"u8)) + using (ImUtf8.PushId("BlendingB"u8)) + { retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } DrawHeader(" Material Template"u8); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("TemplateA"u8)) + using (ImUtf8.PushId("TemplateA"u8)) + { retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("TemplateB"u8)) + using (ImUtf8.PushId("TemplateB"u8)) + { retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } if (dyeTable != null) { DrawHeader(" Dye Properties"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using var columns = ImUtf8.Columns(2, "ColorTable"u8); + using var dis = ImRaii.Disabled(disabled); + using (ImUtf8.PushId("DyeA"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("DyeA"u8)) - retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); - columns.Next(); - using (var id = ImUtf8.PushId("DyeB"u8)) - retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + + columns.Next(); + using (ImUtf8.PushId("DyeB"u8)) + { + retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); } } @@ -177,11 +205,16 @@ public partial class MtrlTab using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { using var dis = ImRaii.Disabled(disabled); - using (var id = ImUtf8.PushId("FurtherA"u8)) + using (ImUtf8.PushId("FurtherA"u8)) + { retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + } + columns.Next(); - using (var id = ImUtf8.PushId("FurtherB"u8)) + using (ImUtf8.PushId("FurtherB"u8)) + { retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + } } if (retA) @@ -195,18 +228,21 @@ public partial class MtrlTab /// Padding styles do not seem to apply to this component. It is recommended to prepend two spaces. private static void DrawHeader(ReadOnlySpan label) { - var headerColor = ImGui.GetColorU32(ImGuiCol.Header); - using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); + var headerColor = ImGui.GetColorU32(ImGuiCol.Header); + using var _ = ImRaii.PushColor(ImGuiCol.HeaderHovered, headerColor).Push(ImGuiCol.HeaderActive, headerColor); ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); } private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { - var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() * 2.0f; + var dyeOffset = ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().ItemSpacing.X + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() * 2.0f; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ret |= CtColorPicker("Diffuse Color"u8, default, row.DiffuseColor, c => table[rowIdx].DiffuseColor = c); @@ -247,13 +283,17 @@ public partial class MtrlTab private static bool DrawBlending(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var dyeOffset = ImGui.GetContentRegionAvail().X + ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var dyeOffset = ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().ItemSpacing.X + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; var isRowB = (rowIdx & 1) != 0; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, @@ -261,11 +301,13 @@ public partial class MtrlTab if (dyeTable != null) { ImGui.SameLine(dyeOffset); - ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, dye.Anisotropy, + ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, + dye.Anisotropy, b => dyeTable[rowIdx].Anisotropy = b); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(scalarSize); - CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, dyePack?.Anisotropy, "%.2f"u8); + CtDragHalf("##dyePreviewAnisotropy"u8, isRowB ? "Dye Preview for Field #19"u8 : "Dye Preview for Anisotropy Degree"u8, + dyePack?.Anisotropy, "%.2f"u8); } return ret; @@ -276,11 +318,11 @@ public partial class MtrlTab var scalarSize = ColorTableScalarSize * UiHelpers.Scale; var itemSpacing = ImGui.GetStyle().ItemSpacing.X; var dyeOffset = ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize - 64.0f; - var subcolWidth = CalculateSubcolumnWidth(2); + var subColWidth = CalculateSubColumnWidth(2); - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, @@ -306,13 +348,15 @@ public partial class MtrlTab ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); ImGui.SetNextItemWidth(scalarSize + itemSpacing + 64.0f); using var dis = ImRaii.Disabled(); - CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); + CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, + Nop); } ImGui.Dummy(new Vector2(64.0f, 0.0f)); ImGui.SameLine(); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, + HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -326,10 +370,10 @@ public partial class MtrlTab ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; + var leftLineHeight = 64.0f + ImGui.GetStyle().FramePadding.Y * 2.0f; var rightLineHeight = 3.0f * ImGui.GetFrameHeight() + 2.0f * ImGui.GetStyle().ItemSpacing.Y; - var lineHeight = Math.Max(leftLineHeight, rightLineHeight); - var cursorPos = ImGui.GetCursorScreenPos(); + var lineHeight = Math.Max(leftLineHeight, rightLineHeight); + var cursorPos = ImGui.GetCursorScreenPos(); ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); ImGui.SetNextItemWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, @@ -337,9 +381,10 @@ public partial class MtrlTab ImUtf8.SameLineInner(); ImUtf8.Text("Tile"u8); - ImGui.SameLine(subcolWidth); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f, }); - using (var cld = ImUtf8.Child("###TileProperties"u8, new(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)), false)) + ImGui.SameLine(subColWidth); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f }); + using (ImUtf8.Child("###TileProperties"u8, + new Vector2(ImGui.GetContentRegionAvail().X, float.Lerp(rightLineHeight, lineHeight, 0.5f)))) { ImGui.Dummy(new Vector2(scalarSize, 0.0f)); ImUtf8.SameLineInner(); @@ -350,7 +395,8 @@ public partial class MtrlTab ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, m => table[rowIdx].TileTransform = m); ImUtf8.SameLineInner(); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); + ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + - new Vector2(0.0f, (ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y) * 0.5f)); ImUtf8.Text("Tile Transform"u8); } @@ -360,15 +406,20 @@ public partial class MtrlTab private static bool DrawPbr(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].Roughness = (Half)(v * 0.01f)); if (dyeTable != null) { @@ -380,13 +431,14 @@ public partial class MtrlTab CtDragScalar("##dyePreviewRoughness"u8, "Dye Preview for Roughness"u8, (float?)dyePack?.Roughness * 100.0f, "%.0f%%"u8); } - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, + 1.0f, v => table[rowIdx].Metalness = (Half)(v * 0.01f)); if (dyeTable != null) { - ImGui.SameLine(subcolWidth + dyeOffset); + ImGui.SameLine(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, b => dyeTable[rowIdx].Metalness = b); ImUtf8.SameLineInner(); @@ -400,12 +452,16 @@ public partial class MtrlTab private static bool DrawSheen(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, @@ -420,13 +476,14 @@ public partial class MtrlTab CtDragScalar("##dyePreviewSheenRate"u8, "Dye Preview for Sheen"u8, (float?)dyePack?.SheenRate * 100.0f, "%.0f%%"u8); } - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, + ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, + HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); if (dyeTable != null) { - ImGui.SameLine(subcolWidth + dyeOffset); + ImGui.SameLine(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, b => dyeTable[rowIdx].SheenTintRate = b); ImUtf8.SameLineInner(); @@ -435,7 +492,8 @@ public partial class MtrlTab } ImGui.SetNextItemWidth(scalarSize); - ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, + ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, + 100.0f / HalfEpsilon, 1.0f, v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); if (dyeTable != null) { @@ -444,7 +502,8 @@ public partial class MtrlTab b => dyeTable[rowIdx].SheenAperture = b); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(scalarSize); - CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, "%.0f%%"u8); + CtDragScalar("##dyePreviewSheenRoughness"u8, "Dye Preview for Sheen Roughness"u8, 100.0f / (float?)dyePack?.SheenAperture, + "%.0f%%"u8); } return ret; @@ -453,12 +512,16 @@ public partial class MtrlTab private static bool DrawFurther(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; - var subcolWidth = CalculateSubcolumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; - var dyeOffset = subcolWidth - ImGui.GetStyle().ItemSpacing.X * 2.0f - ImGui.GetStyle().ItemInnerSpacing.X - ImGui.GetFrameHeight() - scalarSize; + var subColWidth = CalculateSubColumnWidth(2) + ImGui.GetStyle().ItemSpacing.X; + var dyeOffset = subColWidth + - ImGui.GetStyle().ItemSpacing.X * 2.0f + - ImGui.GetStyle().ItemInnerSpacing.X + - ImGui.GetFrameHeight() + - scalarSize; - var ret = false; + var ret = false; ref var row = ref table[rowIdx]; - var dye = dyeTable != null ? dyeTable[rowIdx] : default; + var dye = dyeTable?[rowIdx] ?? default; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, @@ -479,7 +542,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar3 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #7"u8, default, row.Scalar7, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar7 = v); @@ -488,7 +551,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #15"u8, default, row.Scalar15, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar15 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #17"u8, default, row.Scalar17, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar17 = v); @@ -497,7 +560,7 @@ public partial class MtrlTab ret |= CtDragHalf("Field #20"u8, default, row.Scalar20, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar20 = v); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); ret |= CtDragHalf("Field #22"u8, default, row.Scalar22, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar22 = v); @@ -513,33 +576,32 @@ public partial class MtrlTab { var scalarSize = ColorTableScalarSize * UiHelpers.Scale; var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + ImGui.GetStyle().FramePadding.X * 2.0f; - var subcolWidth = CalculateSubcolumnWidth(2, applyButtonWidth); - - var ret = false; + var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth); + + var ret = false; ref var dye = ref dyeTable[rowIdx]; ImGui.SetNextItemWidth(scalarSize); ret |= CtDragScalar("Dye Channel"u8, default, dye.Channel + 1, "%d"u8, 1, StainService.ChannelCount, 0.1f, value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); - ImGui.SameLine(subcolWidth); + ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, - scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) + scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; ret = true; } + ImUtf8.SameLineInner(); ImUtf8.Text("Dye Template"u8); ImGui.SameLine(ImGui.GetContentRegionAvail().X - applyButtonWidth + ImGui.GetStyle().ItemSpacing.X); using var dis = ImRaii.Disabled(!dyePack.HasValue); if (ImUtf8.Button("Apply Preview Dye"u8)) - { ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, ], rowIdx); - } return ret; } @@ -554,9 +616,9 @@ public partial class MtrlTab ImGui.TextUnformatted(text); } - private static float CalculateSubcolumnWidth(int numSubcolumns, float reservedSpace = 0.0f) + private static float CalculateSubColumnWidth(int numSubColumns, float reservedSpace = 0.0f) { var itemSpacing = ImGui.GetStyle().ItemSpacing.X; - return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubcolumns - 1)) / numSubcolumns + itemSpacing; + return (ImGui.GetContentRegionAvail().X - reservedSpace - itemSpacing * (numSubColumns - 1)) / numSubColumns + itemSpacing; } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 2b093e23..09c8ea61 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -12,9 +12,9 @@ namespace Penumbra.UI.AdvancedWindow.Materials; public partial class MtrlTab { - private static readonly float HalfMinValue = (float)Half.MinValue; - private static readonly float HalfMaxValue = (float)Half.MaxValue; - private static readonly float HalfEpsilon = (float)Half.Epsilon; + private static readonly float HalfMinValue = (float)Half.MinValue; + private static readonly float HalfMaxValue = (float)Half.MaxValue; + private static readonly float HalfEpsilon = (float)Half.Epsilon; private static readonly FontAwesomeCheckbox ApplyStainCheckbox = new(FontAwesomeIcon.FillDrip); @@ -22,11 +22,11 @@ public partial class MtrlTab private bool DrawColorTableSection(bool disabled) { - if ((!ShpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId)) || Mtrl.Table == null) + if (!_shpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) return false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (!ImGui.CollapsingHeader("Color Table", ImGuiTreeNodeFlags.DefaultOpen)) + if (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen)) return false; ColorTableCopyAllClipboardButton(); @@ -35,7 +35,7 @@ public partial class MtrlTab if (!disabled) { ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImUtf8.IconDummy(); ImGui.SameLine(); ret |= ColorTableDyeableCheckbox(); } @@ -43,17 +43,18 @@ public partial class MtrlTab if (Mtrl.DyeTable != null) { ImGui.SameLine(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + ImUtf8.IconDummy(); ImGui.SameLine(); ret |= DrawPreviewDye(disabled); } ret |= Mtrl.Table switch { - LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), - ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), - ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), - _ => false, + LegacyColorTable legacyTable => DrawLegacyColorTable(legacyTable, Mtrl.DyeTable as LegacyColorDyeTable, disabled), + ColorTable table when Mtrl.ShaderPackage.Name is "characterlegacy.shpk" => DrawLegacyColorTable(table, + Mtrl.DyeTable as ColorDyeTable, disabled), + ColorTable table => DrawColorTable(table, Mtrl.DyeTable as ColorDyeTable, disabled), + _ => false, }; return ret; @@ -64,7 +65,7 @@ public partial class MtrlTab if (Mtrl.Table == null) return; - if (!ImGui.Button("Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2(200, 0))) + if (!ImUtf8.Button("Export All Rows to Clipboard"u8, ImGuiHelpers.ScaledVector2(200, 0))) return; try @@ -178,16 +179,18 @@ public partial class MtrlTab private bool ColorTableDyeableCheckbox() { var dyeable = Mtrl.DyeTable != null; - var ret = ImGui.Checkbox("Dyeable", ref dyeable); + var ret = ImUtf8.Checkbox("Dyeable"u8, ref dyeable); if (ret) { - Mtrl.DyeTable = dyeable ? Mtrl.Table switch - { - ColorTable => new ColorDyeTable(), - LegacyColorTable => new LegacyColorDyeTable(), - _ => null, - } : null; + Mtrl.DyeTable = dyeable + ? Mtrl.Table switch + { + ColorTable => new ColorDyeTable(), + LegacyColorTable => new LegacyColorDyeTable(), + _ => null, + } + : null; UpdateColorTablePreview(); } @@ -227,24 +230,27 @@ public partial class MtrlTab private void ColorTableHighlightButton(int pairIdx, bool disabled) { - ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, - ImGui.GetFrameHeight() * Vector2.One, disabled || ColorTablePreviewers.Count == 0); + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, + "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || _colorTablePreviewers.Count == 0); if (ImGui.IsItemHovered()) HighlightColorTablePair(pairIdx); - else if (HighlightedColorTablePair == pairIdx) + else if (_highlightedColorTablePair == pairIdx) CancelColorTableHighlight(); } private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) { - var style = ImGui.GetStyle(); - var frameRounding = style.FrameRounding; + var style = ImGui.GetStyle(); + var frameRounding = style.FrameRounding; var frameThickness = style.FrameBorderSize; - var borderColor = ImGui.GetColorU32(ImGuiCol.Border); - var drawList = ImGui.GetWindowDrawList(); + var borderColor = ImGui.GetColorU32(ImGuiCol.Border); + var drawList = ImGui.GetWindowDrawList(); if (topColor == bottomColor) + { drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + } else { drawList.AddRectFilled( @@ -258,10 +264,12 @@ public partial class MtrlTab rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); } + drawList.AddRect(rcMin, rcMax, borderColor, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); } - private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, ReadOnlySpan letter = default) + private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, + ReadOnlySpan letter = default) { var ret = false; var inputSqrt = PseudoSqrtRgb((Vector3)current); @@ -291,10 +299,13 @@ public partial class MtrlTab return ret; } - private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, ReadOnlySpan letter = default) + private static void CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor? current, + ReadOnlySpan letter = default) { if (current.HasValue) + { CtColorPicker(label, description, current.Value, Nop, letter); + } else { var tmp = Vector4.Zero; @@ -308,8 +319,8 @@ public partial class MtrlTab if (letter.Length > 0 && ImGui.IsItemVisible()) { - var textSize = ImUtf8.CalcTextSize(letter); - var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; + var textSize = ImUtf8.CalcTextSize(letter); + var center = ImGui.GetItemRectMin() + (ImGui.GetItemRectSize() - textSize) / 2; ImGui.GetWindowDrawList().AddText(letter, center, 0x80000000u); } @@ -319,7 +330,7 @@ public partial class MtrlTab private static bool CtApplyStainCheckbox(ReadOnlySpan label, ReadOnlySpan description, bool current, Action setter) { - var tmp = current; + var tmp = current; var result = ApplyStainCheckbox.Draw(label, ref tmp); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == current) @@ -329,68 +340,79 @@ public partial class MtrlTab return true; } - private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, float max, float speed, Action setter) + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half value, ReadOnlySpan format, float min, + float max, float speed, Action setter) { - var tmp = (float)value; + var tmp = (float)value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result) return false; + var newValue = (Half)tmp; if (newValue == value) return false; + setter(newValue); return true; } - private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, float min, float max, float speed) + private static bool CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, ref Half value, ReadOnlySpan format, + float min, float max, float speed) { - var tmp = (float)value; + var tmp = (float)value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result) return false; + var newValue = (Half)tmp; if (newValue == value) return false; + value = newValue; return true; } private static void CtDragHalf(ReadOnlySpan label, ReadOnlySpan description, Half? value, ReadOnlySpan format) { - using var _ = ImRaii.Disabled(); - var valueOrDefault = value ?? Half.Zero; - var floatValue = (float)valueOrDefault; + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? Half.Zero; + var floatValue = (float)valueOrDefault; CtDragHalf(label, description, valueOrDefault, value.HasValue ? format : "-"u8, floatValue, floatValue, 0.0f, Nop); } - private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, T max, float speed, Action setter) where T : unmanaged, INumber + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T value, ReadOnlySpan format, T min, + T max, float speed, Action setter) where T : unmanaged, INumber { - var tmp = value; + var tmp = value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == value) return false; + setter(tmp); return true; } - private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, T max, float speed) where T : unmanaged, INumber + private static bool CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, ref T value, ReadOnlySpan format, T min, + T max, float speed) where T : unmanaged, INumber { - var tmp = value; + var tmp = value; var result = ImUtf8.DragScalar(label, ref tmp, format, min, max, speed); ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, description); if (!result || tmp == value) return false; + value = tmp; return true; } - private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) where T : unmanaged, INumber + private static void CtDragScalar(ReadOnlySpan label, ReadOnlySpan description, T? value, ReadOnlySpan format) + where T : unmanaged, INumber { - using var _ = ImRaii.Disabled(); - var valueOrDefault = value ?? T.Zero; + using var _ = ImRaii.Disabled(); + var valueOrDefault = value ?? T.Zero; CtDragScalar(label, description, valueOrDefault, value.HasValue ? format : "-"u8, valueOrDefault, valueOrDefault, 0.0f, Nop); } @@ -398,14 +420,17 @@ public partial class MtrlTab { if (!_materialTemplatePickers.DrawTileIndexPicker(label, description, ref value, compact)) return false; + setter(value); return true; } - private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, Action setter) + private bool CtSphereMapIndexPicker(ReadOnlySpan label, ReadOnlySpan description, ushort value, bool compact, + Action setter) { if (!_materialTemplatePickers.DrawSphereMapIndexPicker(label, description, ref value, compact)) return false; + setter(value); return true; } @@ -430,33 +455,34 @@ public partial class MtrlTab ret |= CtDragHalf("##TileTransformVU"u8, "Tile Skew V"u8, ref tmp.VU, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); if (!ret || tmp == value) return false; + setter(tmp); } else { value.Decompose(out var scale, out var rotation, out var shear); rotation *= 180.0f / MathF.PI; - shear *= 180.0f / MathF.PI; + shear *= 180.0f / MathF.PI; ImGui.SetNextItemWidth(floatSize); var scaleXChanged = CtDragScalar("##TileScaleU"u8, "Tile Scale U"u8, ref scale.X, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); var activated = ImGui.IsItemActivated(); var deactivated = ImGui.IsItemDeactivated(); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var scaleYChanged = CtDragScalar("##TileScaleV"u8, "Tile Scale V"u8, ref scale.Y, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); if (!twoRowLayout) ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var rotationChanged = CtDragScalar("##TileRotation"u8, "Tile Rotation"u8, ref rotation, "%.0f°"u8, -180.0f, 180.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); ImUtf8.SameLineInner(); ImGui.SetNextItemWidth(floatSize); - var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); - activated |= ImGui.IsItemActivated(); - deactivated |= ImGui.IsItemDeactivated(); + var shearChanged = CtDragScalar("##TileShear"u8, "Tile Shear"u8, ref shear, "%.0f°"u8, -90.0f, 90.0f, 1.0f); + activated |= ImGui.IsItemActivated(); + deactivated |= ImGui.IsItemDeactivated(); if (deactivated) _pinnedTileTransform = null; else if (activated) @@ -464,6 +490,7 @@ public partial class MtrlTab ret = scaleXChanged | scaleYChanged | rotationChanged | shearChanged; if (!ret) return false; + if (_pinnedTileTransform.HasValue) { var (pinScale, pinRotation, pinShear) = _pinnedTileTransform.Value; @@ -476,11 +503,14 @@ public partial class MtrlTab if (!shearChanged) shear = pinShear; } + var newValue = HalfMatrix2x2.Compose(scale, rotation * MathF.PI / 180.0f, shear * MathF.PI / 180.0f); if (newValue == value) return false; + setter(newValue); } + return true; } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index 56496005..176ec3f4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -35,7 +35,7 @@ public partial class MtrlTab Constants.Clear(); string mpPrefix; - if (AssociatedShpk == null) + if (_associatedShpk == null) { mpPrefix = MaterialParamsConstantName.Value!; var fcGroup = FindOrAddGroup(Constants, "Further Constants"); @@ -51,12 +51,12 @@ public partial class MtrlTab } else { - mpPrefix = AssociatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; + mpPrefix = _associatedShpk.GetConstantById(MaterialParamsConstantId)?.Name ?? MaterialParamsConstantName.Value!; var autoNameMaxLength = Math.Max(Names.LongestKnownNameLength, mpPrefix.Length + 8); - foreach (var shpkConstant in AssociatedShpk.MaterialParams) + foreach (var shpkConstant in _associatedShpk.MaterialParams) { var name = Names.KnownNames.TryResolve(shpkConstant.Id); - var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, AssociatedShpk, out var constantIndex); + var constant = Mtrl.GetOrAddConstant(shpkConstant.Id, _associatedShpk, out var constantIndex); var values = Mtrl.GetConstantValue(constant); var handledElements = new IndexSet(values.Length, false); @@ -64,8 +64,8 @@ public partial class MtrlTab if (dkData != null) foreach (var dkConstant in dkData) { - var offset = (int)dkConstant.EffectiveByteOffset; - var length = values.Length - offset; + var offset = (int)dkConstant.EffectiveByteOffset; + var length = values.Length - offset; var constantSize = dkConstant.EffectiveByteSize; if (constantSize.HasValue) length = Math.Min(length, (int)constantSize.Value); @@ -86,7 +86,6 @@ public partial class MtrlTab foreach (var (start, end) in handledElements.Ranges(complement: true)) { if (start == 0 && end == values.Length && end - start <= 16) - { if (name.Value != null) { fcGroup.Add(( @@ -94,7 +93,6 @@ public partial class MtrlTab constantIndex, 0..values.Length, string.Empty, true, DefaultConstantEditorFor(name))); continue; } - } if ((shpkConstant.ByteOffset & 0x3) == 0 && (shpkConstant.ByteSize & 0x3) == 0) { @@ -105,7 +103,8 @@ public partial class MtrlTab var rangeEnd = Math.Min(i + 16, end); if (rangeEnd > rangeStart) { - var autoName = $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; + var autoName = + $"{mpPrefix}[{j,2:D}]{VectorSwizzle(((offset + rangeStart) & 0xF) >> 2, ((offset + rangeEnd - 1) & 0xF) >> 2)}"; fcGroup.Add(( $"{autoName.PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, rangeStart..rangeEnd, string.Empty, true, DefaultConstantEditorFor(name))); @@ -116,7 +115,8 @@ public partial class MtrlTab { for (var i = start; i < end; i += 16) { - fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, i..Math.Min(i + 16, end), string.Empty, true, + fcGroup.Add(($"{"???".PadRight(autoNameMaxLength)} (0x{shpkConstant.Id:X8})", constantIndex, + i..Math.Min(i + 16, end), string.Empty, true, DefaultConstantEditorFor(name))); } } @@ -178,15 +178,16 @@ public partial class MtrlTab ret = true; SetMaterialParameter(constant.Id, slice.Start, buffer[slice]); } - var shpkConstant = AssociatedShpk?.GetMaterialParamById(constant.Id); - var defaultConstantValue = shpkConstant.HasValue ? AssociatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; + + var shpkConstant = _associatedShpk?.GetMaterialParamById(constant.Id); + var defaultConstantValue = shpkConstant.HasValue ? _associatedShpk!.GetMaterialParamDefault(shpkConstant.Value) : []; var defaultValue = IsValid(slice, defaultConstantValue.Length) ? defaultConstantValue[slice] : []; - var canReset = AssociatedShpk?.MaterialParamsDefaults != null + var canReset = _associatedShpk?.MaterialParamsDefaults != null ? defaultValue.Length > 0 && !defaultValue.SequenceEqual(buffer[slice]) : buffer[slice].ContainsAnyExcept((byte)0); ImUtf8.SameLineInner(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Backspace.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, - "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) + "Reset this constant to its default value.\n\nHold Ctrl to unlock.", !ImGui.GetIO().KeyCtrl || !canReset, true)) { ret = true; if (defaultValue.Length > 0) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs index cd62d58f..26fe3dcb 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Devkit.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using Newtonsoft.Json.Linq; using OtterGui.Text.Widget.Editors; using Penumbra.String.Classes; @@ -29,8 +30,8 @@ public partial class MtrlTab } private T? TryGetShpkDevkitData(string category, uint? id, bool mayVary) where T : class - => TryGetShpkDevkitData(AssociatedShpkDevkit, LoadedShpkDevkitPathName, category, id, mayVary) - ?? TryGetShpkDevkitData(AssociatedBaseDevkit, LoadedBaseDevkitPathName, category, id, mayVary); + => TryGetShpkDevkitData(_associatedShpkDevkit, _loadedShpkDevkitPathName, category, id, mayVary) + ?? TryGetShpkDevkitData(_associatedBaseDevkit, _loadedBaseDevkitPathName, category, id, mayVary); private T? TryGetShpkDevkitData(JObject? devkit, string devkitPathName, string category, uint? id, bool mayVary) where T : class { @@ -47,7 +48,7 @@ public partial class MtrlTab { var selector = BuildSelector(data!["Vary"]! .Select(key => (uint)key) - .Select(key => Mtrl.GetShaderKey(key)?.Value ?? AssociatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); + .Select(key => Mtrl.GetShaderKey(key)?.Value ?? _associatedShpk!.GetMaterialKeyById(key)!.Value.DefaultValue)); var index = (int)data["Selectors"]![selector.ToString()]!; data = data["Items"]![index]; } @@ -62,12 +63,14 @@ public partial class MtrlTab } } + [UsedImplicitly] private sealed class DevkitShaderKeyValue { public string Label = string.Empty; public string Description = string.Empty; } + [UsedImplicitly] private sealed class DevkitShaderKey { public string Label = string.Empty; @@ -75,6 +78,7 @@ public partial class MtrlTab public Dictionary Values = []; } + [UsedImplicitly] private sealed class DevkitSampler { public string Label = string.Empty; @@ -84,14 +88,16 @@ public partial class MtrlTab private enum DevkitConstantType { - Hidden = -1, - Float = 0, + Hidden = -1, + Float = 0, + /// Integer encoded as a float. - Integer = 1, - Color = 2, - Enum = 3, + Integer = 1, + Color = 2, + Enum = 3, + /// Native integer. - Int32 = 4, + Int32 = 4, Int32Enum = 5, Int8 = 6, Int8Enum = 7, @@ -105,6 +111,7 @@ public partial class MtrlTab SphereMapIndex = 15, } + [UsedImplicitly] private sealed class DevkitConstantValue { public string Label = string.Empty; @@ -112,6 +119,7 @@ public partial class MtrlTab public double Value = 0; } + [UsedImplicitly] private sealed class DevkitConstant { public uint Offset = 0; @@ -147,7 +155,7 @@ public partial class MtrlTab => ByteOffset ?? Offset * ValueSize; public uint? EffectiveByteSize - => ByteSize ?? (Length * ValueSize); + => ByteSize ?? Length * ValueSize; public unsafe uint ValueSize => Type switch @@ -198,19 +206,23 @@ public partial class MtrlTab private IEditor CreateIntegerEditor() where T : unmanaged, INumber => ((Drag || Slider) && !Hex - ? (Drag - ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, Unit, 0) - : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0)) - : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), Hex, Unit, 0)) + ? Drag + ? (IEditor)DragEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), Speed ?? 0.25f, RelativeSpeed, + Unit, 0) + : SliderEditor.CreateInteger(ToInteger(Minimum) ?? default, ToInteger(Maximum) ?? default, Unit, 0) + : InputEditor.CreateInteger(ToInteger(Minimum), ToInteger(Maximum), ToInteger(Step), ToInteger(StepFast), + Hex, Unit, 0)) .WithFactorAndBias(ToInteger(Factor), ToInteger(Bias)); private IEditor CreateFloatEditor() where T : unmanaged, INumber, IPowerFunctions - => ((Drag || Slider) - ? (Drag - ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, Precision, Unit, 0) - : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0)) - : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), T.CreateSaturating(StepFast), Precision, Unit, 0)) + => (Drag || Slider + ? Drag + ? (IEditor)DragEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), Speed ?? 0.1f, RelativeSpeed, + Precision, Unit, 0) + : SliderEditor.CreateFloat(ToFloat(Minimum) ?? default, ToFloat(Maximum) ?? default, Precision, Unit, 0) + : InputEditor.CreateFloat(ToFloat(Minimum), ToFloat(Maximum), T.CreateSaturating(Step), + T.CreateSaturating(StepFast), Precision, Unit, 0)) .WithExponent(T.CreateSaturating(Exponent)) .WithFactorAndBias(T.CreateSaturating(Factor), T.CreateSaturating(Bias)); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index a2165760..0ff2b01f 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -110,9 +110,9 @@ public partial class MtrlTab } ImGui.TableNextColumn(); - using (ImRaii.PushFont(UiBuilder.MonoFont)) - { - ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.Text($"{(rowIdx >> 1) + 1,2:D}{"AB"[rowIdx & 1]}"); } ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 3482e581..6089f2d5 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -12,20 +12,20 @@ namespace Penumbra.UI.AdvancedWindow.Materials; public partial class MtrlTab { - public readonly List MaterialPreviewers = new(4); - public readonly List ColorTablePreviewers = new(4); - public int HighlightedColorTablePair = -1; - public readonly Stopwatch HighlightTime = new(); + private readonly List _materialPreviewers = new(4); + private readonly List _colorTablePreviewers = new(4); + private int _highlightedColorTablePair = -1; + private readonly Stopwatch _highlightTime = new(); private void DrawMaterialLivePreviewRebind(bool disabled) { if (disabled) return; - if (ImGui.Button("Reload live preview")) + if (ImUtf8.Button("Reload live preview"u8)) BindToMaterialInstances(); - if (MaterialPreviewers.Count != 0 || ColorTablePreviewers.Count != 0) + if (_materialPreviewers.Count != 0 || _colorTablePreviewers.Count != 0) return; ImGui.SameLine(); @@ -34,7 +34,7 @@ public partial class MtrlTab "The current material has not been found on your character. Please check the Import from Screen tab for more information."u8); } - public unsafe void BindToMaterialInstances() + private unsafe void BindToMaterialInstances() { UnbindFromMaterialInstances(); @@ -50,7 +50,7 @@ public partial class MtrlTab try { - MaterialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); + _materialPreviewers.Add(new LiveMaterialPreviewer(_objects, materialInfo)); foundMaterials.Add((nint)material); } catch (InvalidOperationException) @@ -68,7 +68,7 @@ public partial class MtrlTab { try { - ColorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); + _colorTablePreviewers.Add(new LiveColorTablePreviewer(_objects, _framework, materialInfo)); } catch (InvalidOperationException) { @@ -81,53 +81,53 @@ public partial class MtrlTab private void UnbindFromMaterialInstances() { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.Dispose(); - MaterialPreviewers.Clear(); + _materialPreviewers.Clear(); - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) previewer.Dispose(); - ColorTablePreviewers.Clear(); + _colorTablePreviewers.Clear(); } private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase) { - for (var i = MaterialPreviewers.Count; i-- > 0;) + for (var i = _materialPreviewers.Count; i-- > 0;) { - var previewer = MaterialPreviewers[i]; + var previewer = _materialPreviewers[i]; if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); - MaterialPreviewers.RemoveAt(i); + _materialPreviewers.RemoveAt(i); } - for (var i = ColorTablePreviewers.Count; i-- > 0;) + for (var i = _colorTablePreviewers.Count; i-- > 0;) { - var previewer = ColorTablePreviewers[i]; + var previewer = _colorTablePreviewers[i]; if (previewer.DrawObject != characterBase) continue; previewer.Dispose(); - ColorTablePreviewers.RemoveAt(i); + _colorTablePreviewers.RemoveAt(i); } } - public void SetShaderPackageFlags(uint shPkFlags) + private void SetShaderPackageFlags(uint shPkFlags) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetShaderPackageFlags(shPkFlags); } - public void SetMaterialParameter(uint parameterCrc, Index offset, Span value) + private void SetMaterialParameter(uint parameterCrc, Index offset, Span value) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetMaterialParameter(parameterCrc, offset, value); } - public void SetSamplerFlags(uint samplerCrc, uint samplerFlags) + private void SetSamplerFlags(uint samplerCrc, uint samplerFlags) { - foreach (var previewer in MaterialPreviewers) + foreach (var previewer in _materialPreviewers) previewer.SetSamplerFlags(samplerCrc, samplerFlags); } @@ -145,14 +145,14 @@ public partial class MtrlTab SetSamplerFlags(sampler.SamplerId, sampler.Flags); } - public void HighlightColorTablePair(int pairIdx) + private void HighlightColorTablePair(int pairIdx) { - var oldPairIdx = HighlightedColorTablePair; + var oldPairIdx = _highlightedColorTablePair; - if (HighlightedColorTablePair != pairIdx) + if (_highlightedColorTablePair != pairIdx) { - HighlightedColorTablePair = pairIdx; - HighlightTime.Restart(); + _highlightedColorTablePair = pairIdx; + _highlightTime.Restart(); } if (oldPairIdx >= 0) @@ -160,19 +160,6 @@ public partial class MtrlTab UpdateColorTableRowPreview(oldPairIdx << 1); UpdateColorTableRowPreview((oldPairIdx << 1) | 1); } - if (pairIdx >= 0) - { - UpdateColorTableRowPreview(pairIdx << 1); - UpdateColorTableRowPreview((pairIdx << 1) | 1); - } - } - - public void CancelColorTableHighlight() - { - var pairIdx = HighlightedColorTablePair; - - HighlightedColorTablePair = -1; - HighlightTime.Reset(); if (pairIdx >= 0) { @@ -181,9 +168,23 @@ public partial class MtrlTab } } - public void UpdateColorTableRowPreview(int rowIdx) + private void CancelColorTableHighlight() { - if (ColorTablePreviewers.Count == 0) + var pairIdx = _highlightedColorTablePair; + + _highlightedColorTablePair = -1; + _highlightTime.Reset(); + + if (pairIdx >= 0) + { + UpdateColorTableRowPreview(pairIdx << 1); + UpdateColorTableRowPreview((pairIdx << 1) | 1); + } + } + + private void UpdateColorTableRowPreview(int rowIdx) + { + if (_colorTablePreviewers.Count == 0) return; if (Mtrl.Table == null) @@ -192,7 +193,7 @@ public partial class MtrlTab var row = Mtrl.Table switch { LegacyColorTable legacyTable => new ColorTableRow(legacyTable[rowIdx]), - ColorTable table => table[rowIdx], + ColorTable table => table[rowIdx], _ => throw new InvalidOperationException($"Unsupported color table type {Mtrl.Table.GetType()}"), }; if (Mtrl.DyeTable != null) @@ -200,8 +201,8 @@ public partial class MtrlTab var dyeRow = Mtrl.DyeTable switch { LegacyColorDyeTable legacyDyeTable => new ColorDyeTableRow(legacyDyeTable[rowIdx]), - ColorDyeTable dyeTable => dyeTable[rowIdx], - _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), + ColorDyeTable dyeTable => dyeTable[rowIdx], + _ => throw new InvalidOperationException($"Unsupported color dye table type {Mtrl.DyeTable.GetType()}"), }; if (dyeRow.Channel < StainService.ChannelCount) { @@ -213,21 +214,21 @@ public partial class MtrlTab } } - if (HighlightedColorTablePair << 1 == rowIdx) - ApplyHighlight(ref row, ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); - else if (((HighlightedColorTablePair << 1) | 1) == rowIdx) - ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + if (_highlightedColorTablePair << 1 == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + else if (((_highlightedColorTablePair << 1) | 1) == rowIdx) + ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)_highlightTime.Elapsed.TotalSeconds); - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) { row[..].CopyTo(previewer.GetColorRow(rowIdx)); previewer.ScheduleUpdate(); } } - public void UpdateColorTablePreview() + private void UpdateColorTablePreview() { - if (ColorTablePreviewers.Count == 0) + if (_colorTablePreviewers.Count == 0) return; if (Mtrl.Table == null) @@ -237,7 +238,8 @@ public partial class MtrlTab var dyeRows = Mtrl.DyeTable != null ? ColorDyeTable.CastOrConvert(Mtrl.DyeTable) : null; if (dyeRows != null) { - ReadOnlySpan stainIds = [ + ReadOnlySpan stainIds = + [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, ]; @@ -245,13 +247,14 @@ public partial class MtrlTab rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); } - if (HighlightedColorTablePair >= 0) + if (_highlightedColorTablePair >= 0) { - ApplyHighlight(ref rows[HighlightedColorTablePair << 1], ColorId.InGameHighlight, (float)HighlightTime.Elapsed.TotalSeconds); - ApplyHighlight(ref rows[(HighlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, (float)HighlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[_highlightedColorTablePair << 1], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + ApplyHighlight(ref rows[(_highlightedColorTablePair << 1) | 1], ColorId.InGameHighlight2, + (float)_highlightTime.Elapsed.TotalSeconds); } - foreach (var previewer in ColorTablePreviewers) + foreach (var previewer in _colorTablePreviewers) { rows.AsHalves().CopyTo(previewer.ColorTable); previewer.ScheduleUpdate(); @@ -260,11 +263,11 @@ public partial class MtrlTab private static void ApplyHighlight(ref ColorTableRow row, ColorId colorId, float time) { - var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; - var baseColor = colorId.Value(); + var level = (MathF.Sin(time * 2.0f * MathF.PI) + 2.0f) / 3.0f / 255.0f; + var baseColor = colorId.Value(); var color = level * new Vector3(baseColor & 0xFF, (baseColor >> 8) & 0xFF, (baseColor >> 16) & 0xFF); var halfColor = (HalfColor)(color * color); - + row.DiffuseColor = halfColor; row.SpecularColor = halfColor; row.EmissiveColor = halfColor; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index 21557939..ae57a122 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -21,8 +21,8 @@ public partial class MtrlTab // Apricot shader packages are unlisted because // 1. they cause severe performance/memory issues when calculating the effective shader set // 2. they probably aren't intended for use with materials anyway - internal static readonly IReadOnlyList StandardShaderPackages = new[] - { + private static readonly IReadOnlyList StandardShaderPackages = + [ "3dui.shpk", // "apricot_decal_dummy.shpk", // "apricot_decal_ring.shpk", @@ -80,35 +80,35 @@ public partial class MtrlTab "verticalfog.shpk", "water.shpk", "weather.shpk", - }; + ]; - private static readonly byte[] UnknownShadersString = Encoding.UTF8.GetBytes("Vertex Shaders: ???\nPixel Shaders: ???"); + private static readonly byte[] UnknownShadersString = "Vertex Shaders: ???\nPixel Shaders: ???"u8.ToArray(); private string[]? _shpkNames; - public string ShaderHeader = "Shader###Shader"; - public FullPath LoadedShpkPath = FullPath.Empty; - public string LoadedShpkPathName = string.Empty; - public string LoadedShpkDevkitPathName = string.Empty; - public string ShaderComment = string.Empty; - public ShpkFile? AssociatedShpk; - public bool ShpkLoading; - public JObject? AssociatedShpkDevkit; + private string _shaderHeader = "Shader###Shader"; + private FullPath _loadedShpkPath = FullPath.Empty; + private string _loadedShpkPathName = string.Empty; + private string _loadedShpkDevkitPathName = string.Empty; + private string _shaderComment = string.Empty; + private ShpkFile? _associatedShpk; + private bool _shpkLoading; + private JObject? _associatedShpkDevkit; - public readonly string LoadedBaseDevkitPathName; - public readonly JObject? AssociatedBaseDevkit; + private readonly string _loadedBaseDevkitPathName; + private readonly JObject? _associatedBaseDevkit; // Shader Key State - public readonly + private readonly List<(string Label, int Index, string Description, bool MonoFont, IReadOnlyList<(string Label, uint Value, string Description)> - Values)> ShaderKeys = new(16); + Values)> _shaderKeys = new(16); - public readonly HashSet VertexShaders = new(16); - public readonly HashSet PixelShaders = new(16); - public bool ShadersKnown; - public ReadOnlyMemory ShadersString = UnknownShadersString; + private readonly HashSet _vertexShaders = new(16); + private readonly HashSet _pixelShaders = new(16); + private bool _shadersKnown; + private ReadOnlyMemory _shadersString = UnknownShadersString; - public string[] GetShpkNames() + private string[] GetShpkNames() { if (null != _shpkNames) return _shpkNames; @@ -122,7 +122,7 @@ public partial class MtrlTab return _shpkNames; } - public FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) + private FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) @@ -131,45 +131,45 @@ public partial class MtrlTab return _edit.FindBestMatch(defaultGamePath); } - public void LoadShpk(FullPath path) + private void LoadShpk(FullPath path) => Task.Run(() => DoLoadShpk(path)); private async Task DoLoadShpk(FullPath path) { - ShadersKnown = false; - ShaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; - ShpkLoading = true; + _shadersKnown = false; + _shaderHeader = $"Shader ({Mtrl.ShaderPackage.Name})###Shader"; + _shpkLoading = true; try { var data = path.IsRooted ? await File.ReadAllBytesAsync(path.FullName) : _gameData.GetFile(path.InternalName.ToString())?.Data; - LoadedShpkPath = path; - AssociatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); - LoadedShpkPathName = path.ToPath(); + _loadedShpkPath = path; + _associatedShpk = data?.Length > 0 ? new ShpkFile(data) : throw new Exception("Failure to load file data."); + _loadedShpkPathName = path.ToPath(); } catch (Exception e) { - LoadedShpkPath = FullPath.Empty; - LoadedShpkPathName = string.Empty; - AssociatedShpk = null; - Penumbra.Messager.NotificationMessage(e, $"Could not load {LoadedShpkPath.ToPath()}.", NotificationType.Error, false); + _loadedShpkPath = FullPath.Empty; + _loadedShpkPathName = string.Empty; + _associatedShpk = null; + Penumbra.Messager.NotificationMessage(e, $"Could not load {_loadedShpkPath.ToPath()}.", NotificationType.Error, false); } finally { - ShpkLoading = false; + _shpkLoading = false; } - if (LoadedShpkPath.InternalName.IsEmpty) + if (_loadedShpkPath.InternalName.IsEmpty) { - AssociatedShpkDevkit = null; - LoadedShpkDevkitPathName = string.Empty; + _associatedShpkDevkit = null; + _loadedShpkDevkitPathName = string.Empty; } else { - AssociatedShpkDevkit = - TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out LoadedShpkDevkitPathName); + _associatedShpkDevkit = + TryLoadShpkDevkit(Path.GetFileNameWithoutExtension(Mtrl.ShaderPackage.Name), out _loadedShpkDevkitPathName); } UpdateShaderKeys(); @@ -178,9 +178,9 @@ public partial class MtrlTab private void UpdateShaderKeys() { - ShaderKeys.Clear(); - if (AssociatedShpk != null) - foreach (var key in AssociatedShpk.MaterialKeys) + _shaderKeys.Clear(); + if (_associatedShpk != null) + foreach (var key in _associatedShpk.MaterialKeys) { var keyName = Names.KnownNames.TryResolve(key.Id); var dkData = TryGetShpkDevkitData("ShaderKeys", key.Id, false); @@ -210,7 +210,7 @@ public partial class MtrlTab return string.Compare(x.Label, y.Label, StringComparison.Ordinal); }); - ShaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, + _shaderKeys.Add((hasDkLabel ? dkData!.Label : keyName.ToString(), mtrlKeyIndex, dkData?.Description ?? string.Empty, !hasDkLabel, values)); } else @@ -218,7 +218,7 @@ public partial class MtrlTab { var keyName = Names.KnownNames.TryResolve(key.Category); var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); - ShaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); + _shaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); } } @@ -232,27 +232,28 @@ public partial class MtrlTab passSet = []; byPassSets.Add(passId, passSet); } + passSet.Add(shaderIndex); } - VertexShaders.Clear(); - PixelShaders.Clear(); + _vertexShaders.Clear(); + _pixelShaders.Clear(); var vertexShadersByPass = new Dictionary>(); var pixelShadersByPass = new Dictionary>(); - if (AssociatedShpk == null || !AssociatedShpk.IsExhaustiveNodeAnalysisFeasible()) + if (_associatedShpk == null || !_associatedShpk.IsExhaustiveNodeAnalysisFeasible()) { - ShadersKnown = false; + _shadersKnown = false; } else { - ShadersKnown = true; - var systemKeySelectors = AllSelectors(AssociatedShpk.SystemKeys).ToArray(); - var sceneKeySelectors = AllSelectors(AssociatedShpk.SceneKeys).ToArray(); - var subViewKeySelectors = AllSelectors(AssociatedShpk.SubViewKeys).ToArray(); + _shadersKnown = true; + var systemKeySelectors = AllSelectors(_associatedShpk.SystemKeys).ToArray(); + var sceneKeySelectors = AllSelectors(_associatedShpk.SceneKeys).ToArray(); + var subViewKeySelectors = AllSelectors(_associatedShpk.SubViewKeys).ToArray(); var materialKeySelector = - BuildSelector(AssociatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); + BuildSelector(_associatedShpk.MaterialKeys.Select(key => Mtrl.GetOrAddShaderKey(key.Id, key.DefaultValue).Value)); foreach (var systemKeySelector in systemKeySelectors) { @@ -261,38 +262,39 @@ public partial class MtrlTab foreach (var subViewKeySelector in subViewKeySelectors) { var selector = BuildSelector(systemKeySelector, sceneKeySelector, materialKeySelector, subViewKeySelector); - var node = AssociatedShpk.GetNodeBySelector(selector); + var node = _associatedShpk.GetNodeBySelector(selector); if (node.HasValue) foreach (var pass in node.Value.Passes) { - AddShader(VertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); - AddShader(PixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); + AddShader(_vertexShaders, vertexShadersByPass, pass.Id, (int)pass.VertexShader); + AddShader(_pixelShaders, pixelShadersByPass, pass.Id, (int)pass.PixelShader); } else - ShadersKnown = false; + _shadersKnown = false; } } } } - if (ShadersKnown) + if (_shadersKnown) { var builder = new StringBuilder(); - foreach (var (passId, passVS) in vertexShadersByPass) + foreach (var (passId, passVertexShader) in vertexShadersByPass) { if (builder.Length > 0) builder.Append("\n\n"); var passName = Names.KnownNames.TryResolve(passId); - var shaders = passVS.OrderBy(i => i).Select(i => $"#{i}"); + var shaders = passVertexShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"Vertex Shaders ({passName}): {string.Join(", ", shaders)}"); - if (pixelShadersByPass.TryGetValue(passId, out var passPS)) + if (pixelShadersByPass.TryGetValue(passId, out var passPixelShader)) { - shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"\nPixel Shaders ({passName}): {string.Join(", ", shaders)}"); } } - foreach (var (passId, passPS) in pixelShadersByPass) + + foreach (var (passId, passPixelShader) in pixelShadersByPass) { if (vertexShadersByPass.ContainsKey(passId)) continue; @@ -301,22 +303,24 @@ public partial class MtrlTab builder.Append("\n\n"); var passName = Names.KnownNames.TryResolve(passId); - var shaders = passPS.OrderBy(i => i).Select(i => $"#{i}"); + var shaders = passPixelShader.OrderBy(i => i).Select(i => $"#{i}"); builder.Append($"Pixel Shaders ({passName}): {string.Join(", ", shaders)}"); } - ShadersString = Encoding.UTF8.GetBytes(builder.ToString()); + _shadersString = Encoding.UTF8.GetBytes(builder.ToString()); } else - ShadersString = UnknownShadersString; + { + _shadersString = UnknownShadersString; + } - ShaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; + _shaderComment = TryGetShpkDevkitData("Comment", null, true) ?? string.Empty; } private bool DrawShaderSection(bool disabled) { var ret = false; - if (ImGui.CollapsingHeader(ShaderHeader)) + if (ImGui.CollapsingHeader(_shaderHeader)) { ret |= DrawPackageNameInput(disabled); ret |= DrawShaderFlagsInput(disabled); @@ -325,20 +329,17 @@ public partial class MtrlTab DrawMaterialShaders(); } - if (!ShpkLoading && (AssociatedShpk == null || AssociatedShpkDevkit == null)) + if (!_shpkLoading && (_associatedShpk == null || _associatedShpkDevkit == null)) { ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if (AssociatedShpk == null) - { + if (_associatedShpk == null) ImUtf8.Text("Unable to find a suitable shader (.shpk) file for cross-references. Some functionality will be missing."u8, ImGuiUtil.HalfBlendText(0x80u)); // Half red - } else - { - ImUtf8.Text("No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."u8, + ImUtf8.Text( + "No dev-kit file found for this material's shaders. Please install one for optimal editing experience, such as actual constant names instead of hexadecimal identifiers."u8, ImGuiUtil.HalfBlendText(0x8080u)); // Half yellow - } } return ret; @@ -358,14 +359,14 @@ public partial class MtrlTab if (c) foreach (var value in GetShpkNames()) { - if (ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) - { - Mtrl.ShaderPackage.Name = value; - ret = true; - AssociatedShpk = null; - LoadedShpkPath = FullPath.Empty; - LoadShpk(FindAssociatedShpk(out _, out _)); - } + if (!ImGui.Selectable(value, value == Mtrl.ShaderPackage.Name)) + continue; + + Mtrl.ShaderPackage.Name = value; + ret = true; + _associatedShpk = null; + _loadedShpkPath = FullPath.Empty; + LoadShpk(FindAssociatedShpk(out _, out _)); } return ret; @@ -391,23 +392,23 @@ public partial class MtrlTab private void DrawCustomAssociations() { const string tooltip = "Click to copy file path to clipboard."; - var text = AssociatedShpk == null + var text = _associatedShpk == null ? "Associated .shpk file: None" - : $"Associated .shpk file: {LoadedShpkPathName}"; - var devkitText = AssociatedShpkDevkit == null + : $"Associated .shpk file: {_loadedShpkPathName}"; + var devkitText = _associatedShpkDevkit == null ? "Associated dev-kit file: None" - : $"Associated dev-kit file: {LoadedShpkDevkitPathName}"; - var baseDevkitText = AssociatedBaseDevkit == null + : $"Associated dev-kit file: {_loadedShpkDevkitPathName}"; + var baseDevkitText = _associatedBaseDevkit == null ? "Base dev-kit file: None" - : $"Base dev-kit file: {LoadedBaseDevkitPathName}"; + : $"Base dev-kit file: {_loadedBaseDevkitPathName}"; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGuiUtil.CopyOnClickSelectable(text, LoadedShpkPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(devkitText, LoadedShpkDevkitPathName, tooltip); - ImGuiUtil.CopyOnClickSelectable(baseDevkitText, LoadedBaseDevkitPathName, tooltip); + ImUtf8.CopyOnClickSelectable(text, _loadedShpkPathName, tooltip); + ImUtf8.CopyOnClickSelectable(devkitText, _loadedShpkDevkitPathName, tooltip); + ImUtf8.CopyOnClickSelectable(baseDevkitText, _loadedBaseDevkitPathName, tooltip); - if (ImGui.Button("Associate Custom .shpk File")) + if (ImUtf8.Button("Associate Custom .shpk File"u8)) _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { if (success) @@ -416,15 +417,15 @@ public partial class MtrlTab var moddedPath = FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), - moddedPath.Equals(LoadedShpkPath))) + if (ImUtf8.ButtonEx("Associate Default .shpk File"u8, moddedPath.ToPath(), Vector2.Zero, + moddedPath.Equals(_loadedShpkPath))) LoadShpk(moddedPath); if (!gamePath.Path.Equals(moddedPath.InternalName)) { ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, - gamePath.Path.Equals(LoadedShpkPath.InternalName))) + if (ImUtf8.ButtonEx("Associate Unmodded .shpk File", defaultPath, Vector2.Zero, + gamePath.Path.Equals(_loadedShpkPath.InternalName))) LoadShpk(new FullPath(gamePath)); } @@ -433,22 +434,23 @@ public partial class MtrlTab private bool DrawMaterialShaderKeys(bool disabled) { - if (ShaderKeys.Count == 0) + if (_shaderKeys.Count == 0) return false; var ret = false; - foreach (var (label, index, description, monoFont, values) in ShaderKeys) + foreach (var (label, index, description, monoFont, values) in _shaderKeys) { using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; - var shpkKey = AssociatedShpk?.GetMaterialKeyById(key.Category); + using var id = ImUtf8.PushId((int)key.Category); + var shpkKey = _associatedShpk?.GetMaterialKeyById(key.Category); var currentValue = key.Value; var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); if (!disabled && shpkKey.HasValue) { ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f); - using (var c = ImRaii.Combo($"##{key.Category:X8}", currentLabel)) + using (var c = ImUtf8.Combo(""u8, currentLabel)) { if (c) foreach (var (valueLabel, value, valueDescription) in values) @@ -469,16 +471,16 @@ public partial class MtrlTab if (description.Length > 0) ImGuiUtil.LabeledHelpMarker(label, description); else - ImGui.TextUnformatted(label); + ImUtf8.Text(label); } else if (description.Length > 0 || currentDescription.Length > 0) { - ImGuiUtil.LabeledHelpMarker($"{label}: {currentLabel}", + ImUtf8.LabeledHelpMarker($"{label}: {currentLabel}", description + (description.Length > 0 && currentDescription.Length > 0 ? "\n\n" : string.Empty) + currentDescription); } else { - ImGui.TextUnformatted($"{label}: {currentLabel}"); + ImUtf8.Text($"{label}: {currentLabel}"); } } @@ -487,19 +489,19 @@ public partial class MtrlTab private void DrawMaterialShaders() { - if (AssociatedShpk == null) + if (_associatedShpk == null) return; using (var node = ImUtf8.TreeNode("Candidate Shaders"u8)) { if (node) - ImUtf8.Text(ShadersString.Span); + ImUtf8.Text(_shadersString.Span); } - if (ShaderComment.Length > 0) + if (_shaderComment.Length > 0) { ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - ImGui.TextUnformatted(ShaderComment); + ImUtf8.Text(_shaderComment); } } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 3181dafe..7ab2900d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -23,7 +23,7 @@ public partial class MtrlTab { Textures.Clear(); SamplerIds.Clear(); - if (AssociatedShpk == null) + if (_associatedShpk == null) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) @@ -34,11 +34,11 @@ public partial class MtrlTab } else { - foreach (var index in VertexShaders) - SamplerIds.UnionWith(AssociatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in PixelShaders) - SamplerIds.UnionWith(AssociatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!ShadersKnown) + foreach (var index in _vertexShaders) + SamplerIds.UnionWith(_associatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); + foreach (var index in _pixelShaders) + SamplerIds.UnionWith(_associatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + if (!_shadersKnown) { SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) @@ -47,11 +47,11 @@ public partial class MtrlTab foreach (var samplerId in SamplerIds) { - var shpkSampler = AssociatedShpk.GetSamplerById(samplerId); + var shpkSampler = _associatedShpk.GetSamplerById(samplerId); if (shpkSampler is not { Slot: 2 }) continue; - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); @@ -95,9 +95,12 @@ public partial class MtrlTab private static ReadOnlySpan TextureAddressModeTooltip(TextureAddressMode addressMode) => addressMode switch { - TextureAddressMode.Wrap => "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, - TextureAddressMode.Mirror => "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on."u8, - TextureAddressMode.Clamp => "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, + TextureAddressMode.Wrap => + "Tile the texture at every UV integer junction.\n\nFor example, for U values between 0 and 3, the texture is repeated three times."u8, + TextureAddressMode.Mirror => + "Flip the texture at every UV integer junction.\n\nFor U values between 0 and 1, for example, the texture is addressed normally; between 1 and 2, the texture is mirrored; between 2 and 3, the texture is normal again; and so on."u8, + TextureAddressMode.Clamp => + "Texture coordinates outside the range [0.0, 1.0] are set to the texture color at 0.0 or 1.0, respectively."u8, TextureAddressMode.Border => "Texture coordinates outside the range [0.0, 1.0] are set to the border color (generally black)."u8, _ => ""u8, }; @@ -167,7 +170,7 @@ public partial class MtrlTab return ret; } - + private static bool ComboTextureAddressMode(ReadOnlySpan label, ref TextureAddressMode value) { using var c = ImUtf8.Combo(label, value.ToString()); @@ -202,7 +205,7 @@ public partial class MtrlTab ret = true; } - ref var samplerFlags = ref SamplerFlags.Wrap(ref sampler.Flags); + ref var samplerFlags = ref Wrap(ref sampler.Flags); ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); var addressMode = samplerFlags.UAddressMode; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 2d4e93f1..6e16de99 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; -using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Interop; @@ -21,7 +20,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable private const int ShpkPrefixLength = 16; private static readonly CiByteString ShpkPrefix = CiByteString.FromSpanUnsafe("shader/sm5/shpk/"u8, true, true, true); - + private readonly IDataManager _gameData; private readonly IFramework _framework; private readonly ObjectManager _objects; @@ -40,7 +39,8 @@ public sealed partial class MtrlTab : IWritable, IDisposable private bool _updateOnNextFrame; public unsafe MtrlTab(IDataManager gameData, IFramework framework, ObjectManager objects, CharacterBaseDestructor characterBaseDestructor, - StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, + StainService stainService, ResourceTreeFactory resourceTreeFactory, FileDialogService fileDialog, + MaterialTemplatePickers materialTemplatePickers, Configuration config, ModEditWindow edit, MtrlFile file, string filePath, bool writable) { _gameData = gameData; @@ -53,11 +53,11 @@ public sealed partial class MtrlTab : IWritable, IDisposable _materialTemplatePickers = materialTemplatePickers; _config = config; - _edit = edit; - Mtrl = file; - FilePath = filePath; - Writable = writable; - AssociatedBaseDevkit = TryLoadShpkDevkit("_base", out LoadedBaseDevkitPathName); + _edit = edit; + Mtrl = file; + FilePath = filePath; + Writable = writable; + _associatedBaseDevkit = TryLoadShpkDevkit("_base", out _loadedBaseDevkitPathName); Update(); LoadShpk(FindAssociatedShpk(out _, out _)); if (writable) @@ -118,7 +118,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable using var dis = ImRaii.Disabled(disabled); var tmp = shaderFlags.EnableTransparency; - if (ImGui.Checkbox("Enable Transparency", ref tmp)) + if (ImUtf8.Checkbox("Enable Transparency"u8, ref tmp)) { shaderFlags.EnableTransparency = tmp; ret = true; @@ -127,14 +127,14 @@ public sealed partial class MtrlTab : IWritable, IDisposable ImGui.SameLine(200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); tmp = shaderFlags.HideBackfaces; - if (ImGui.Checkbox("Hide Backfaces", ref tmp)) + if (ImUtf8.Checkbox("Hide Backfaces"u8, ref tmp)) { shaderFlags.HideBackfaces = tmp; ret = true; SetShaderPackageFlags(Mtrl.ShaderPackage.Flags); } - if (ShpkLoading) + if (_shpkLoading) { ImGui.SameLine(400 * UiHelpers.Scale + 2 * ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X); @@ -147,32 +147,32 @@ public sealed partial class MtrlTab : IWritable, IDisposable private void DrawOtherMaterialDetails(bool _) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImUtf8.CollapsingHeader("Further Content"u8)) return; - using (var sets = ImRaii.TreeNode("UV Sets", ImGuiTreeNodeFlags.DefaultOpen)) + using (var sets = ImUtf8.TreeNode("UV Sets"u8, ImGuiTreeNodeFlags.DefaultOpen)) { if (sets) foreach (var set in Mtrl.UvSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - using (var sets = ImRaii.TreeNode("Color Sets", ImGuiTreeNodeFlags.DefaultOpen)) + using (var sets = ImUtf8.TreeNode("Color Sets"u8, ImGuiTreeNodeFlags.DefaultOpen)) { if (sets) foreach (var set in Mtrl.ColorSets) - ImRaii.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf).Dispose(); } if (Mtrl.AdditionalData.Length <= 0) return; - using var t = ImRaii.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); + using var t = ImUtf8.TreeNode($"Additional Data (Size: {Mtrl.AdditionalData.Length})###AdditionalData"); if (t) Widget.DrawHexViewer(Mtrl.AdditionalData); } - public void Update() + private void Update() { UpdateShaders(); UpdateTextures(); @@ -187,12 +187,12 @@ public sealed partial class MtrlTab : IWritable, IDisposable } public bool Valid - => ShadersKnown && Mtrl.Valid; + => _shadersKnown && Mtrl.Valid; public byte[] Write() { var output = Mtrl.Clone(); - output.GarbageCollect(AssociatedShpk, SamplerIds); + output.GarbageCollect(_associatedShpk, SamplerIds); return output.Write(); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs index af8b7db2..09db4277 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -8,9 +8,16 @@ using Penumbra.Services; namespace Penumbra.UI.AdvancedWindow.Materials; -public sealed class MtrlTabFactory(IDataManager gameData, IFramework framework, ObjectManager objects, - CharacterBaseDestructor characterBaseDestructor, StainService stainService, ResourceTreeFactory resourceTreeFactory, - FileDialogService fileDialog, MaterialTemplatePickers materialTemplatePickers, Configuration config) : IUiService +public sealed class MtrlTabFactory( + IDataManager gameData, + IFramework framework, + ObjectManager objects, + CharacterBaseDestructor characterBaseDestructor, + StainService stainService, + ResourceTreeFactory resourceTreeFactory, + FileDialogService fileDialog, + MaterialTemplatePickers materialTemplatePickers, + Configuration config) : IUiService { public MtrlTab Create(ModEditWindow edit, MtrlFile file, string filePath, bool writable) => new(gameData, framework, objects, characterBaseDestructor, stainService, resourceTreeFactory, fileDialog, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index ee883daf..59b38465 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.AdvancedWindow; @@ -24,7 +25,7 @@ public partial class ModEditWindow if (_editor.Files.Mdl.Count == 0) return; - using var tab = ImRaii.TabItem("Material Reassignment"); + using var tab = ImUtf8.TabItem("Material Reassignment"u8); if (!tab) return; @@ -32,45 +33,43 @@ public partial class ModEditWindow MaterialSuffix.Draw(_editor, ImGuiHelpers.ScaledVector2(175, 0)); ImGui.NewLine(); - using var child = ImRaii.Child("##mdlFiles", -Vector2.One, true); + using var child = ImUtf8.Child("##mdlFiles"u8, -Vector2.One, true); if (!child) return; - using var table = ImRaii.Table("##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + using var table = ImUtf8.Table("##files"u8, 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); if (!table) return; - var iconSize = ImGui.GetFrameHeight() * Vector2.One; foreach (var (info, idx) in _editor.MdlMaterialEditor.ModelFiles.WithIndex()) { using var id = ImRaii.PushId(idx); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Save the changed mdl file.\nUse at own risk!"u8, disabled: !info.Changed)) info.Save(_editor.Compactor); ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.Recycle, "Restore current changes to default."u8, disabled: !info.Changed)) info.Restore(); ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); + ImUtf8.Text(info.Path.InternalName.Span[(Mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); var tmp = info.CurrentMaterials[0]; - if (ImGui.InputText("##0", ref tmp, 64)) + if (ImUtf8.InputText("##0"u8, ref tmp)) info.SetMaterial(tmp, 0); for (var i = 1; i < info.Count; ++i) { + using var id2 = ImUtf8.PushId(i); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); tmp = info.CurrentMaterials[i]; - if (ImGui.InputText($"##{i}", ref tmp, 64)) + if (ImUtf8.InputText(""u8, ref tmp)) info.SetMaterial(tmp, i); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 8a1c729c..41f1da26 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -10,7 +10,6 @@ using Penumbra.GameData.Interop; using Penumbra.String; using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; -using Penumbra.GameData.Files.ShaderStructs; using OtterGui.Text; using Penumbra.GameData.Structs; @@ -56,21 +55,17 @@ public partial class ModEditWindow private static void DrawShaderPackageSummary(ShpkTab tab) { if (tab.Shpk.IsLegacy) - { ImUtf8.Text("This legacy shader package will not work in the current version of the game. Do not attempt to load it.", ImGuiUtil.HalfBlendText(0x80u)); // Half red - } - ImGui.TextUnformatted(tab.Header); + ImUtf8.Text(tab.Header); if (!tab.Shpk.Disassembled) - { ImUtf8.Text("Your system doesn't support disassembling shaders. Some functionality will be missing.", ImGuiUtil.HalfBlendText(0x80u)); // Half red - } } private static void DrawShaderExportButton(ShpkTab tab, string objectName, Shader shader, int idx) { - if (!ImGui.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) + if (!ImUtf8.Button($"Export Shader Program Blob ({shader.Blob.Length} bytes)")) return; var defaultName = objectName[0] switch @@ -106,7 +101,7 @@ public partial class ModEditWindow private static void DrawShaderImportButton(ShpkTab tab, string objectName, Shader[] shaders, int idx) { - if (!ImGui.Button("Replace Shader Program Blob")) + if (!ImUtf8.Button("Replace Shader Program Blob"u8)) return; tab.FileDialog.OpenFilePicker($"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", @@ -145,8 +140,8 @@ public partial class ModEditWindow private static unsafe void DrawRawDisassembly(Shader shader) { - using var t2 = ImRaii.TreeNode("Raw Program Disassembly"); - if (!t2) + using var tree = ImUtf8.TreeNode("Raw Program Disassembly"u8); + if (!tree) return; using var font = ImRaii.PushFont(UiBuilder.MonoFont); @@ -164,31 +159,34 @@ public partial class ModEditWindow { foreach (var (key, keyIdx) in shader.SystemValues!.WithIndex()) { - ImRaii.TreeNode($"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.SceneValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.MaterialValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode( + $"Used with Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in shader.SubViewValues!.WithIndex()) { - ImRaii.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", + ImUtf8.TreeNode($"Used with Sub-View Key #{keyIdx} \u2208 {{ {tab.NameSetToString(key)} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } } - ImRaii.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used in Passes: {tab.NameSetToString(shader.Passes)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } private static void DrawShaderPackageFilterSection(ShpkTab tab) @@ -215,19 +213,20 @@ public partial class ModEditWindow { if (values.PossibleValues == null) { - ImRaii.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(label, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); return; } - using var node = ImRaii.TreeNode(label); + using var node = ImUtf8.TreeNode(label); if (!node) return; foreach (var value in values.PossibleValues) { var contains = values.Contains(value); - if (!ImGui.Checkbox($"{tab.TryResolveName(value)}", ref contains)) + if (!ImUtf8.Checkbox($"{tab.TryResolveName(value)}", ref contains)) continue; + if (contains) { if (values.AddExisting(value)) @@ -249,7 +248,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageShaderArray(ShpkTab tab, string objectName, Shader[] shaders, bool disabled) { - if (shaders.Length == 0 || !ImGui.CollapsingHeader($"{objectName}s")) + if (shaders.Length == 0 || !ImUtf8.CollapsingHeader($"{objectName}s")) return false; var ret = false; @@ -259,7 +258,7 @@ public partial class ModEditWindow if (!tab.IsFilterMatch(shader)) continue; - using var t = ImRaii.TreeNode($"{objectName} #{idx}"); + using var t = ImUtf8.TreeNode($"{objectName} #{idx}"); if (!t) continue; @@ -270,20 +269,20 @@ public partial class ModEditWindow DrawShaderImportButton(tab, objectName, shaders, idx); } - ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); - ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "slot", true, shader.Constants, false, true); + ret |= DrawShaderPackageResourceArray("Samplers", "slot", false, shader.Samplers, false, true); if (!tab.Shpk.IsLegacy) - ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); + ret |= DrawShaderPackageResourceArray("Textures", "slot", false, shader.Textures, false, true); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "slot", true, shader.Uavs, false, true); if (shader.DeclaredInputs != 0) - ImRaii.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Declared Inputs: {shader.DeclaredInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (shader.UsedInputs != 0) - ImRaii.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used Inputs: {shader.UsedInputs}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (shader.AdditionalHeader.Length > 8) { - using var t2 = ImRaii.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); + using var t2 = ImUtf8.TreeNode($"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader"); if (t2) Widget.DrawHexViewer(shader.AdditionalHeader); } @@ -313,23 +312,28 @@ public partial class ModEditWindow var usedString = UsedComponentString(withSize, false, resource); if (usedString.Length > 0) { - ImRaii.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(hasFilter ? $"Globally Used: {usedString}" : $"Used: {usedString}", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (hasFilter) { var filteredUsedString = UsedComponentString(withSize, true, resource); if (filteredUsedString.Length > 0) - ImRaii.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Used within Filters: {filteredUsedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); else - ImRaii.TreeNode("Unused within Filters", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode("Unused within Filters"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } } else - ImRaii.TreeNode(hasFilter ? "Globally Unused" : "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + { + ImUtf8.TreeNode(hasFilter ? "Globally Unused"u8 : "Unused"u8, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + } return ret; } - private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, bool disabled) + private static bool DrawShaderPackageResourceArray(string arrayName, string slotLabel, bool withSize, Resource[] resources, bool hasFilter, + bool disabled) { if (resources.Length == 0) return false; @@ -345,8 +349,8 @@ public partial class ModEditWindow var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}" + (withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty); using var font = ImRaii.PushFont(UiBuilder.MonoFont); - using var t2 = ImRaii.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); - font.Dispose(); + using var t2 = ImUtf8.TreeNode(name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + font.Pop(); if (t2) ret |= DrawShaderPackageResource(slotLabel, withSize, ref buf, hasFilter, disabled); } @@ -361,7 +365,7 @@ public partial class ModEditWindow + new Vector2(ImGui.CalcTextSize(label).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y); - var ret = ImGui.CollapsingHeader(label); + var ret = ImUtf8.CollapsingHeader(label); ImGui.GetWindowDrawList() .AddText(UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), "Layout"); return ret; @@ -374,7 +378,7 @@ public partial class ModEditWindow if (isSizeWellDefined) return true; - ImGui.TextUnformatted(materialParams.HasValue + ImUtf8.Text(materialParams.HasValue ? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)" : $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16"); return false; @@ -382,7 +386,7 @@ public partial class ModEditWindow private static bool DrawShaderPackageMaterialMatrix(ShpkTab tab, bool disabled) { - ImGui.TextUnformatted(tab.Shpk.Disassembled + ImUtf8.Text(tab.Shpk.Disassembled ? "Parameter positions (continuations are grayed out, globally unused values are red, unused values within filters are yellow):" : "Parameter positions (continuations are grayed out):"); @@ -398,10 +402,7 @@ public partial class ModEditWindow ImGui.TableSetupColumn("w", ImGuiTableColumnFlags.WidthFixed, 250 * UiHelpers.Scale); ImGui.TableHeadersRow(); - var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); - var textColorCont = ImGuiUtil.HalfTransparent(textColorStart); // Half opacity - var textColorUnusedStart = ImGuiUtil.HalfBlend(textColorStart, 0x80u); // Half red - var textColorUnusedCont = ImGuiUtil.HalfTransparent(textColorUnusedStart); + var textColorStart = ImGui.GetColorU32(ImGuiCol.Text); var ret = false; for (var i = 0; i < tab.Matrix.GetLength(0); ++i) @@ -420,12 +421,12 @@ public partial class ModEditWindow color = ImGuiUtil.HalfTransparent(color); // Half opacity using var _ = ImRaii.PushId(i * 4 + j); var deletable = !disabled && idx >= 0; - using (var font = ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) + using (ImRaii.PushFont(UiBuilder.MonoFont, tooltip.Length > 0)) { - using (var c = ImRaii.PushColor(ImGuiCol.Text, color)) + using (ImRaii.PushColor(ImGuiCol.Text, color)) { ImGui.TableNextColumn(); - ImGui.Selectable(name); + ImUtf8.Selectable(name); if (deletable && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) { tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems(idx); @@ -434,11 +435,11 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip(tooltip); + ImUtf8.HoverTooltip(tooltip); } if (deletable) - ImGuiUtil.HoverTooltip("\nControl + Right-Click to remove."); + ImUtf8.HoverTooltip("\nControl + Right-Click to remove."u8); } } @@ -450,7 +451,9 @@ public partial class ModEditWindow if (!ImUtf8.Button("Export globally unused parameters as material dev-kit file"u8)) return; - tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", ".json", DoSave, null, false); + tab.FileDialog.OpenSavePicker("Export material dev-kit file", ".json", $"{Path.GetFileNameWithoutExtension(tab.FilePath)}.json", + ".json", DoSave, null, false); + return; void DoSave(bool success, string path) { @@ -476,22 +479,22 @@ public partial class ModEditWindow private static void DrawShaderPackageMisalignedParameters(ShpkTab tab) { - using var t = ImRaii.TreeNode("Misaligned / Overflowing Parameters"); + using var t = ImUtf8.TreeNode("Misaligned / Overflowing Parameters"u8); if (!t) return; using var _ = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var name in tab.MalformedParameters) - ImRaii.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode(name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } private static void DrawShaderPackageStartCombo(ShpkTab tab) { using var s = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing); - using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) + using (ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - using var c = ImRaii.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); + using var c = ImUtf8.Combo("##Start", tab.Orphans[tab.NewMaterialParamStart].Name); if (c) foreach (var (start, idx) in tab.Orphans.WithIndex()) { @@ -501,7 +504,7 @@ public partial class ModEditWindow } ImGui.SameLine(); - ImGui.TextUnformatted("Start"); + ImUtf8.Text("Start"u8); } private static void DrawShaderPackageEndCombo(ShpkTab tab) @@ -510,7 +513,7 @@ public partial class ModEditWindow using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.SetNextItemWidth(UiHelpers.Scale * 400); - using var c = ImRaii.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); + using var c = ImUtf8.Combo("##End", tab.Orphans[tab.NewMaterialParamEnd].Name); if (c) { var current = tab.Orphans[tab.NewMaterialParamStart].Index; @@ -527,7 +530,7 @@ public partial class ModEditWindow } ImGui.SameLine(); - ImGui.TextUnformatted("End"); + ImUtf8.Text("End"u8); } private static bool DrawShaderPackageNewParameter(ShpkTab tab) @@ -540,15 +543,14 @@ public partial class ModEditWindow ImGui.SetNextItemWidth(UiHelpers.Scale * 400); var newName = tab.NewMaterialParamName.Value!; - if (ImGui.InputText("Name", ref newName, 63)) + if (ImUtf8.InputText("Name", ref newName)) tab.NewMaterialParamName = newName; var tooltip = tab.UsedIds.Contains(tab.NewMaterialParamName.Crc32) - ? "The ID is already in use. Please choose a different name." - : string.Empty; - if (!ImGuiUtil.DrawDisabledButton($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), - tooltip, - tooltip.Length > 0)) + ? "The ID is already in use. Please choose a different name."u8 + : ""u8; + if (!ImUtf8.ButtonEx($"Add {tab.NewMaterialParamName} (0x{tab.NewMaterialParamName.Crc32:X8})", tooltip, + new Vector2(400 * UiHelpers.Scale, ImGui.GetFrameHeight()), tooltip.Length > 0)) return false; tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem(new MaterialParam @@ -589,15 +591,15 @@ public partial class ModEditWindow { var ret = false; - if (!ImGui.CollapsingHeader("Shader Resources")) + if (!ImUtf8.CollapsingHeader("Shader Resources"u8)) return false; var hasFilters = tab.FilterPopCount != tab.FilterMaximumPopCount; - ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); - ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Constant Buffers", "type", true, tab.Shpk.Constants, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Samplers", "type", false, tab.Shpk.Samplers, hasFilters, disabled); if (!tab.Shpk.IsLegacy) - ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); - ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Textures", "type", false, tab.Shpk.Textures, hasFilters, disabled); + ret |= DrawShaderPackageResourceArray("Unordered Access Views", "type", false, tab.Shpk.Uavs, hasFilters, disabled); return ret; } @@ -607,18 +609,20 @@ public partial class ModEditWindow if (keys.Count == 0) return; - using var t = ImRaii.TreeNode(arrayName); + using var t = ImUtf8.TreeNode(arrayName); if (!t) return; using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var (key, idx) in keys.WithIndex()) { - using var t2 = ImRaii.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); + using var t2 = ImUtf8.TreeNode(withId ? $"#{idx}: {tab.TryResolveName(key.Id)} (0x{key.Id:X8})" : $"#{idx}"); if (t2) { - ImRaii.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); - ImRaii.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Default Value: {tab.TryResolveName(key.DefaultValue)} (0x{key.DefaultValue:X8})", + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Known Values: {tab.NameSetToString(key.Values, true)}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + .Dispose(); } } } @@ -628,7 +632,7 @@ public partial class ModEditWindow if (tab.Shpk.Nodes.Length <= 0) return; - using var t = ImRaii.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); + using var t = ImUtf8.TreeNode($"Nodes ({tab.Shpk.Nodes.Length})###Nodes"); if (!t) return; @@ -639,39 +643,44 @@ public partial class ModEditWindow if (!tab.IsFilterMatch(node)) continue; - using var t2 = ImRaii.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); + using var t2 = ImUtf8.TreeNode($"#{idx:D4}: Selector: 0x{node.Selector:X8}"); if (!t2) continue; foreach (var (key, keyIdx) in node.SystemKeys.WithIndex()) { - ImRaii.TreeNode($"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"System Key {tab.TryResolveName(tab.Shpk.SystemKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SystemValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SceneKeys.WithIndex()) { - ImRaii.TreeNode($"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Scene Key {tab.TryResolveName(tab.Shpk.SceneKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SceneValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.MaterialKeys.WithIndex()) { - ImRaii.TreeNode($"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Material Key {tab.TryResolveName(tab.Shpk.MaterialKeys[keyIdx].Id)} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.MaterialValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } foreach (var (key, keyIdx) in node.SubViewKeys.WithIndex()) { - ImRaii.TreeNode($"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", + ImUtf8.TreeNode( + $"Sub-View Key #{keyIdx} = {tab.TryResolveName(key)} / \u2208 {{ {tab.NameSetToString(node.SubViewValues![keyIdx])} }}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); } - ImRaii.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", + ImUtf8.TreeNode($"Pass Indices: {string.Join(' ', node.PassIndices.Select(c => $"{c:X2}"))}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); foreach (var (pass, passIdx) in node.Passes.WithIndex()) { - ImRaii.TreeNode($"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", + ImUtf8.TreeNode( + $"Pass #{passIdx}: ID: {tab.TryResolveName(pass.Id)}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } @@ -680,7 +689,7 @@ public partial class ModEditWindow private static void DrawShaderPackageSelection(ShpkTab tab) { - if (!ImGui.CollapsingHeader("Shader Selection")) + if (!ImUtf8.CollapsingHeader("Shader Selection"u8)) return; DrawKeyArray(tab, "System Keys", true, tab.Shpk.SystemKeys); @@ -689,13 +698,13 @@ public partial class ModEditWindow DrawKeyArray(tab, "Sub-View Keys", false, tab.Shpk.SubViewKeys); DrawShaderPackageNodes(tab); - using var t = ImRaii.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); + using var t = ImUtf8.TreeNode($"Node Selectors ({tab.Shpk.NodeSelectors.Count})###NodeSelectors"); if (t) { using var font = ImRaii.PushFont(UiBuilder.MonoFont); foreach (var selector in tab.Shpk.NodeSelectors) { - ImRaii.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) + ImUtf8.TreeNode($"#{selector.Value:D4}: Selector: 0x{selector.Key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet) .Dispose(); } } @@ -703,14 +712,14 @@ public partial class ModEditWindow private static void DrawOtherShaderPackageDetails(ShpkTab tab) { - if (!ImGui.CollapsingHeader("Further Content")) + if (!ImUtf8.CollapsingHeader("Further Content"u8)) return; - ImRaii.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); + ImUtf8.TreeNode($"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet).Dispose(); if (tab.Shpk.AdditionalData.Length > 0) { - using var t = ImRaii.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); + using var t = ImUtf8.TreeNode($"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData"); if (t) Widget.DrawHexViewer(tab.Shpk.AdditionalData); } @@ -718,9 +727,9 @@ public partial class ModEditWindow private static string UsedComponentString(bool withSize, bool filtered, in Resource resource) { - var used = filtered ? resource.FilteredUsed : resource.Used; + var used = filtered ? resource.FilteredUsed : resource.Used; var usedDynamically = filtered ? resource.FilteredUsedDynamically : resource.UsedDynamically; - var sb = new StringBuilder(256); + var sb = new StringBuilder(256); if (withSize) { foreach (var (components, i) in (used ?? Array.Empty()).WithIndex()) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index de20aa9f..b5b39e90 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -21,11 +21,11 @@ public partial class ModEditWindow public short NewMaterialParamStart; public short NewMaterialParamEnd; - public SharedSet[] FilterSystemValues; - public SharedSet[] FilterSceneValues; - public SharedSet[] FilterMaterialValues; - public SharedSet[] FilterSubViewValues; - public SharedSet FilterPasses; + public readonly SharedSet[] FilterSystemValues; + public readonly SharedSet[] FilterSceneValues; + public readonly SharedSet[] FilterMaterialValues; + public readonly SharedSet[] FilterSubViewValues; + public SharedSet FilterPasses; public readonly int FilterMaximumPopCount; public int FilterPopCount; @@ -46,6 +46,7 @@ public partial class ModEditWindow { Shpk = new ShpkFile(bytes, false); } + FilePath = filePath; Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; @@ -105,13 +106,21 @@ public partial class ModEditWindow _nameSetWithIdsCache.Clear(); } - public void UpdateNameCache() + private void UpdateNameCache() { - static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) - { - foreach (var resource in resources) - nameCache.TryAdd(resource.Id, resource.Name); - } + CollectResourceNames(_nameCache, Shpk.Constants); + CollectResourceNames(_nameCache, Shpk.Samplers); + CollectResourceNames(_nameCache, Shpk.Textures); + CollectResourceNames(_nameCache, Shpk.Uavs); + + CollectKeyNames(_nameCache, Shpk.SystemKeys); + CollectKeyNames(_nameCache, Shpk.SceneKeys); + CollectKeyNames(_nameCache, Shpk.MaterialKeys); + CollectKeyNames(_nameCache, Shpk.SubViewKeys); + + _nameSetCache.Clear(); + _nameSetWithIdsCache.Clear(); + return; static void CollectKeyNames(Dictionary nameCache, ShpkFile.Key[] keys) { @@ -128,18 +137,11 @@ public partial class ModEditWindow } } - CollectResourceNames(_nameCache, Shpk.Constants); - CollectResourceNames(_nameCache, Shpk.Samplers); - CollectResourceNames(_nameCache, Shpk.Textures); - CollectResourceNames(_nameCache, Shpk.Uavs); - - CollectKeyNames(_nameCache, Shpk.SystemKeys); - CollectKeyNames(_nameCache, Shpk.SceneKeys); - CollectKeyNames(_nameCache, Shpk.MaterialKeys); - CollectKeyNames(_nameCache, Shpk.SubViewKeys); - - _nameSetCache.Clear(); - _nameSetWithIdsCache.Clear(); + static void CollectResourceNames(Dictionary nameCache, ShpkFile.Resource[] resources) + { + foreach (var resource in resources) + nameCache.TryAdd(resource.Id, resource.Name); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -151,6 +153,7 @@ public partial class ModEditWindow var cache = withIds ? _nameSetWithIdsCache : _nameSetCache; if (cache.TryGetValue(nameSet, out var nameSetStr)) return nameSetStr; + if (withIds) nameSetStr = string.Join(", ", nameSet.Select(id => $"{TryResolveName(id)} (0x{id:X8})")); else @@ -186,7 +189,8 @@ public partial class ModEditWindow var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add($"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); + MalformedParameters.Add( + $"ID: {TryResolveName(param.Id)} (0x{param.Id:X8}), offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } @@ -206,7 +210,8 @@ public partial class ModEditWindow var tt = $"{MtrlTab.MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} ({TryResolveName(param.Id)}, 0x{param.Id:X8})"; if (component < defaultFloats.Length) - tt += $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; + tt += + $"\n\nDefault value: {defaultFloats[component]} ({defaults[component << 2]:X2} {defaults[(component << 2) | 1]:X2} {defaults[(component << 2) | 2]:X2} {defaults[(component << 2) | 3]:X2})"; Matrix[i, j] = (TryResolveName(param.Id).ToString(), tt, (short)idx, 0); } } @@ -265,7 +270,8 @@ public partial class ModEditWindow if (oldStart == linear) newMaterialParamStart = (short)Orphans.Count; - Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", linear)); + Orphans.Add(($"{materialParams?.Name ?? ShpkFile.MaterialParamsConstantName}{MtrlTab.MaterialParamName(false, linear)}", + linear)); } } @@ -407,7 +413,6 @@ public partial class ModEditWindow var unusedSlices = new JArray(); if (materialParameterUsage.Indices(start, length).Any()) - { foreach (var (rgStart, rgEnd) in materialParameterUsage.Ranges(start, length, true)) { unusedSlices.Add(new JObject @@ -417,14 +422,11 @@ public partial class ModEditWindow ["Length"] = rgEnd - rgStart, }); } - } else - { unusedSlices.Add(new JObject { ["Type"] = "Hidden", }); - } dkConstants[param.Id.ToString()] = unusedSlices; } From 6d42673aa4a3d9e289cc7d1ed1a8993ce1980a9e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 22:52:53 +0200 Subject: [PATCH 325/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f2734d54..8ee82929 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f2734d543d9b2debecb8feb6d6fa928801eb2bcb +Subproject commit 8ee82929fa6c725b8f556904ba022fb418991b5c From e91e0b23f8ffd9e01e9e139786ff0f7167a9e78a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 4 Aug 2024 23:18:18 +0200 Subject: [PATCH 326/865] Unused usings. --- Penumbra/Penumbra.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 557e011c..438cdc49 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -17,10 +17,8 @@ using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; using OtterGui.Tasks; -using Penumbra.GameData.Enums; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; -using System.Xml.Linq; using Dalamud.Plugin.Services; using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; From 0064c4c96e04f29a90018e84e3a1fd3747eef0b7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 4 Aug 2024 21:23:13 +0000 Subject: [PATCH 327/865] [CI] Updating repo.json for testing_1.2.0.20 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cbe2b121..ad1cbef0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.19", + "TestingAssemblyVersion": "1.2.0.20", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.19/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.20/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a5859761906db84e24ae1fcf8648530f9940c5aa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Aug 2024 00:41:39 +0200 Subject: [PATCH 328/865] Make ImcChecker threadsafe. --- Penumbra/Meta/ImcChecker.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 751113a0..4e3ff11b 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -12,12 +12,16 @@ public class ImcChecker public static int GetVariantCount(ImcIdentifier identifier) { - if (VariantCounts.TryGetValue(identifier, out var count)) - return count; + lock (VariantCounts) + { + if (VariantCounts.TryGetValue(identifier, out var count)) + return count; - count = GetFile(identifier)?.Count ?? 0; - VariantCounts[identifier] = count; - return count; + count = GetFile(identifier)?.Count ?? 0; + VariantCounts[identifier] = count; + + return count; + } } public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); From 2534f119e9c6a35c5280309f64cb3bdd9289d8c7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 5 Aug 2024 00:41:55 +0200 Subject: [PATCH 329/865] Make StainService deal with early-loading. --- Penumbra/Services/StainService.cs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index ba5c3e63..0a437da0 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,8 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack + : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. public int CurrentDyeChannel = 0; @@ -80,11 +81,21 @@ public class StainService : IService public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo1 = CreateStainCombo(); - StainCombo2 = CreateStainCombo(); - LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); - GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + StainData = stainData; + StainCombo1 = CreateStainCombo(); + StainCombo2 = CreateStainCombo(); + + if (characterUtility.Address == null) + { + LegacyStmFile = LoadStmFile(null, dataManager); + GudStmFile = LoadStmFile(null, dataManager); + } + else + { + LegacyStmFile = LoadStmFile(characterUtility.Address->LegacyStmResource, dataManager); + GudStmFile = LoadStmFile(characterUtility.Address->GudStmResource, dataManager); + } + FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; @@ -98,11 +109,13 @@ public class StainService : IService { 0 => StainCombo1, 1 => StainCombo2, - _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, $"Unsupported dye channel {channel} (supported values are 0 and 1)") + _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, + $"Unsupported dye channel {channel} (supported values are 0 and 1)"), }; /// Loads a STM file. Opportunistically attempts to re-use the file already read by the game, with Lumina fallback. - private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) where TDyePack : unmanaged, IDyePack + private static unsafe StmFile LoadStmFile(ResourceHandle* stmResourceHandle, IDataManager dataManager) + where TDyePack : unmanaged, IDyePack { if (stmResourceHandle != null) { From 1b5553284c102449bbd986be10e57259f2cb4bf4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 4 Aug 2024 22:44:50 +0000 Subject: [PATCH 330/865] [CI] Updating repo.json for testing_1.2.0.21 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ad1cbef0..10a08fa1 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.20", + "TestingAssemblyVersion": "1.2.0.21", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.20/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.21/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 700fef4f04c283aada11ac2379bde2d5de7fb98a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 03:41:35 +0200 Subject: [PATCH 331/865] Move hook to MaterialResourceHandle.Load (inlining my beloathed) --- Penumbra.GameData | 2 +- .../{MtrlShpkLoaded.cs => MtrlLoaded.cs} | 4 ++-- Penumbra/Interop/Hooks/HookSettings.cs | 2 +- .../Hooks/PostProcessing/ShaderReplacementFixer.cs | 6 +++--- .../Resources/{LoadMtrlShpk.cs => LoadMtrl.cs} | 14 +++++++------- Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs | 1 + Penumbra/Services/CommunicatorService.cs | 6 +++--- 7 files changed, 18 insertions(+), 17 deletions(-) rename Penumbra/Communication/{MtrlShpkLoaded.cs => MtrlLoaded.cs} (73%) rename Penumbra/Interop/Hooks/Resources/{LoadMtrlShpk.cs => LoadMtrl.cs} (55%) diff --git a/Penumbra.GameData b/Penumbra.GameData index 8ee82929..ac9d9c78 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 8ee82929fa6c725b8f556904ba022fb418991b5c +Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947 diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlLoaded.cs similarity index 73% rename from Penumbra/Communication/MtrlShpkLoaded.cs rename to Penumbra/Communication/MtrlLoaded.cs index 9d3597a8..78498844 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlLoaded.cs @@ -6,11 +6,11 @@ namespace Penumbra.Communication; /// Parameter is the material resource handle for which the shader package has been loaded. /// Parameter is the associated game object. /// -public sealed class MtrlShpkLoaded() : EventWrapper(nameof(MtrlShpkLoaded)) +public sealed class MtrlLoaded() : EventWrapper(nameof(MtrlLoaded)) { public enum Priority { - /// + /// ShaderReplacementFixer = 0, } } diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 0c0a4020..0bc55dc5 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -99,7 +99,7 @@ public class HookOverrides public struct ResourceHooks { public bool ApricotResourceLoad; - public bool LoadMtrlShpk; + public bool LoadMtrl; public bool LoadMtrlTex; public bool ResolvePathHooks; public bool ResourceHandleDestructor; diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 53b69741..d02e18bb 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -110,7 +110,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; - _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); + _communicator.MtrlLoaded.Subscribe(OnMtrlLoaded, MtrlLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } @@ -118,7 +118,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic { _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); - _communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded); + _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); _hairMaskState.ClearMaterials(); _characterOcclusionState.ClearMaterials(); @@ -147,7 +147,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan); } - private void OnMtrlShpkLoaded(nint mtrlResourceHandle, nint gameObject) + private void OnMtrlLoaded(nint mtrlResourceHandle, nint gameObject) { var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; var shpk = mtrl->ShaderPackageResourceHandle; diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrl.cs similarity index 55% rename from Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs rename to Penumbra/Interop/Hooks/Resources/LoadMtrl.cs index 8c410ad8..f56177e4 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlShpk.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrl.cs @@ -5,28 +5,28 @@ using Penumbra.Services; namespace Penumbra.Interop.Hooks.Resources; -public sealed unsafe class LoadMtrlShpk : FastHook +public sealed unsafe class LoadMtrl : FastHook { private readonly GameState _gameState; private readonly CommunicatorService _communicator; - public LoadMtrlShpk(HookManager hooks, GameState gameState, CommunicatorService communicator) + public LoadMtrl(HookManager hooks, GameState gameState, CommunicatorService communicator) { _gameState = gameState; _communicator = communicator; - Task = hooks.CreateHook("Load Material Shaders", Sigs.LoadMtrlShpk, Detour, !HookOverrides.Instance.Resources.LoadMtrlShpk); + Task = hooks.CreateHook("Load Material", Sigs.LoadMtrl, Detour, !HookOverrides.Instance.Resources.LoadMtrl); } - public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle); + public delegate byte Delegate(MaterialResourceHandle* mtrlResourceHandle, void* unk1, byte unk2); - private byte Detour(MaterialResourceHandle* handle) + private byte Detour(MaterialResourceHandle* handle, void* unk1, byte unk2) { var last = _gameState.MtrlData.Value; var mtrlData = _gameState.LoadSubFileHelper((nint)handle); _gameState.MtrlData.Value = mtrlData; - var ret = Task.Result.Original(handle); + var ret = Task.Result.Original(handle, unk1, unk2); _gameState.MtrlData.Value = last; - _communicator.MtrlShpkLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject); + _communicator.MtrlLoaded.Invoke((nint)handle, mtrlData.AssociatedGameObject); return ret; } } diff --git a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs index 0759d9b1..1866e859 100644 --- a/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs +++ b/Penumbra/Interop/Hooks/Resources/LoadMtrlTex.cs @@ -4,6 +4,7 @@ using Penumbra.GameData; namespace Penumbra.Interop.Hooks.Resources; +// TODO check if this is still needed, as our hooked function is called by LoadMtrl's hooked function public sealed unsafe class LoadMtrlTex : FastHook { private readonly GameState _gameState; diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index cacbe689..5d745419 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -24,8 +24,8 @@ public class CommunicatorService : IDisposable, IService /// public readonly CreatedCharacterBase CreatedCharacterBase = new(); - /// - public readonly MtrlShpkLoaded MtrlShpkLoaded = new(); + /// + public readonly MtrlLoaded MtrlLoaded = new(); /// public readonly ModDataChanged ModDataChanged = new(); @@ -87,7 +87,7 @@ public class CommunicatorService : IDisposable, IService TemporaryGlobalModChange.Dispose(); CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); - MtrlShpkLoaded.Dispose(); + MtrlLoaded.Dispose(); ModDataChanged.Dispose(); ModOptionChanged.Dispose(); ModDiscoveryStarted.Dispose(); From dba85f5da3774706f6005ddd56859bc78362afef Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 03:43:18 +0200 Subject: [PATCH 332/865] Sanity check ShPk mods, ban incompatible ones --- .../Processing/ShpkPathPreProcessor.cs | 85 +++++++++++++++++++ Penumbra/Mods/Manager/ModManager.cs | 18 ++++ Penumbra/UI/ChatWarningService.cs | 56 ++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 Penumbra/Interop/Processing/ShpkPathPreProcessor.cs create mode 100644 Penumbra/UI/ChatWarningService.cs diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs new file mode 100644 index 00000000..2c6f6901 --- /dev/null +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -0,0 +1,85 @@ +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.Utility; +using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.String; +using Penumbra.String.Classes; +using Penumbra.UI; + +namespace Penumbra.Interop.Processing; + +/// +/// Path pre-processor for shader packages that reverts redirects to known invalid files, as bad ShPks can crash the game. +/// +public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, ChatWarningService chatWarningService) : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Shpk; + + public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + { + chatWarningService.CleanLastFileWarnings(false); + + if (!resolved.HasValue) + return null; + + // Skip the sanity check for game files. We are not considering the case where the user has modified game file: it's at their own risk. + var resolvedPath = resolved.Value; + if (!resolvedPath.IsRooted) + return resolvedPath; + + // If the ShPk is already loaded, it means that it already passed the sanity check. + var existingResource = resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32)); + if (existingResource != null) + return resolvedPath; + + var checkResult = SanityCheck(resolvedPath.FullName); + if (checkResult == SanityCheckResult.Success) + return resolvedPath; + + Penumbra.Log.Warning($"Refusing to honor file redirection because of failed sanity check (result: {checkResult}). Original path: {originalGamePath} Redirected path: {resolvedPath}"); + chatWarningService.PrintFileWarning(resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult)); + + return null; + } + + private static SanityCheckResult SanityCheck(string path) + { + try + { + using var file = MmioMemoryManager.CreateFromFile(path); + var bytes = file.GetSpan(); + + return ShpkFile.FastIsLegacy(bytes) + ? SanityCheckResult.Legacy + : SanityCheckResult.Success; + } + catch (FileNotFoundException) + { + return SanityCheckResult.NotFound; + } + catch (IOException) + { + return SanityCheckResult.IoError; + } + } + + private static string WarningMessageComplement(SanityCheckResult result) + => result switch + { + SanityCheckResult.IoError => "cannot read the modded file.", + SanityCheckResult.NotFound => "the modded file does not exist.", + SanityCheckResult.Legacy => "this mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", + _ => string.Empty, + }; + + private enum SanityCheckResult + { + Success, + IoError, + NotFound, + Legacy, + } +} diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index f170a31b..59f8906e 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -350,4 +350,22 @@ public sealed class ModManager : ModStorage, IDisposable, IService Penumbra.Log.Error($"Could not scan for mods:\n{ex}"); } } + + public bool TryIdentifyPath(string path, [NotNullWhen(true)] out Mod? mod, [NotNullWhen(true)] out string? relativePath) + { + var relPath = Path.GetRelativePath(BasePath.FullName, path); + if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath))) + { + mod = null; + relativePath = null; + return false; + } + + var modDirectorySeparator = relPath.IndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]); + + var modDirectory = modDirectorySeparator < 0 ? relPath : relPath[..modDirectorySeparator]; + relativePath = modDirectorySeparator < 0 ? string.Empty : relPath[(modDirectorySeparator + 1)..]; + + return TryGetMod(modDirectory, "\0", out mod); + } } diff --git a/Penumbra/UI/ChatWarningService.cs b/Penumbra/UI/ChatWarningService.cs new file mode 100644 index 00000000..84ede2fb --- /dev/null +++ b/Penumbra/UI/ChatWarningService.cs @@ -0,0 +1,56 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; + +namespace Penumbra.UI; + +public sealed class ChatWarningService(IChatGui chatGui, IClientState clientState, ModManager modManager) : IUiService +{ + private readonly Dictionary _lastFileWarnings = []; + private int _lastFileWarningsCleanCounter; + + private const int LastFileWarningsCleanCycle = 100; + private static readonly TimeSpan LastFileWarningsMaxAge = new(1, 0, 0); + + public void CleanLastFileWarnings(bool force) + { + if (!force) + { + _lastFileWarningsCleanCounter = (_lastFileWarningsCleanCounter + 1) % LastFileWarningsCleanCycle; + if (_lastFileWarningsCleanCounter != 0) + return; + } + + var expiredDate = DateTime.Now - LastFileWarningsMaxAge; + var toRemove = new HashSet(); + foreach (var (key, value) in _lastFileWarnings) + { + if (value.Item1 <= expiredDate) + toRemove.Add(key); + } + foreach (var key in toRemove) + _lastFileWarnings.Remove(key); + } + + public void PrintFileWarning(string fullPath, Utf8GamePath originalGamePath, string messageComplement) + { + CleanLastFileWarnings(true); + + // Don't warn twice for the same file within a certain time interval unless the reason changed. + if (_lastFileWarnings.TryGetValue(fullPath, out var lastWarning) && lastWarning.Item2 == messageComplement) + return; + + // Don't warn for files managed by other plugins, or files we aren't sure about. + if (!modManager.TryIdentifyPath(fullPath, out var mod, out _)) + return; + + // Don't warn if there's no local player (as an approximation of no chat), so as not to trigger the cooldown. + if (clientState.LocalPlayer == null) + return; + + // The wording is an allusion to tar's "Cowardly refusing to create an empty archive" + chatGui.PrintError($"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ": " : ".")}{messageComplement}", "Penumbra"); + _lastFileWarnings[fullPath] = (DateTime.Now, messageComplement); + } +} From a36f9ccec7f4a1adf6e95c484da5a5a9ae9c2d3b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 03:45:02 +0200 Subject: [PATCH 333/865] Improve ResourceTree display with new function --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 ++++ .../ResourceTree/ResourceTreeFactory.cs | 19 ++++++++++++++++++- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 5 ++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 6ab48325..85d12ce7 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -15,6 +15,8 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; + public string? ModName; + public string? ModRelativePath; public CiByteString AdditionalData; public readonly ulong Length; public readonly List Children; @@ -57,6 +59,8 @@ public class ResourceNode : ICloneable ResourceHandle = other.ResourceHandle; PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; + ModName = other.ModName; + ModRelativePath = other.ModRelativePath; AdditionalData = other.AdditionalData; Length = other.Length; Children = other.Children; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 65fac68f..9738148f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -9,6 +9,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Meta; +using Penumbra.Mods.Manager; using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; @@ -21,7 +22,8 @@ public class ResourceTreeFactory( ObjectIdentification objectIdentifier, Configuration config, ActorManager actors, - PathState pathState) : IService + PathState pathState, + ModManager modManager) : IService { private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); @@ -93,7 +95,10 @@ public class ResourceTreeFactory( // This is currently unneeded as we can resolve all paths by querying the draw object: // ResolveGamePaths(tree, collectionResolveData.ModCollection); if (globalContext.WithUiData) + { ResolveUiData(tree); + ResolveModData(tree); + } FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? config.ModDirectory : null); Cleanup(tree); @@ -123,6 +128,18 @@ public class ResourceTreeFactory( }); } + private void ResolveModData(ResourceTree tree) + { + foreach (var node in tree.FlatNodes) + { + if (node.FullPath.IsRooted && modManager.TryIdentifyPath(node.FullPath.FullName, out var mod, out var relativePath)) + { + node.ModName = mod.Name; + node.ModRelativePath = relativePath; + } + } + } + private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath) { foreach (var node in tree.FlatNodes) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index a991c948..361094c4 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -316,7 +316,10 @@ public class ResourceTreeViewer ImGui.TableNextColumn(); if (resourceNode.FullPath.FullName.Length > 0) { - ImGui.Selectable(resourceNode.FullPath.ToPath(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + var uiFullPathStr = resourceNode.ModName != null && resourceNode.ModRelativePath != null + ? $"[{resourceNode.ModName}] {resourceNode.ModRelativePath}" + : resourceNode.FullPath.ToPath(); + ImGui.Selectable(uiFullPathStr, false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); ImGuiUtil.HoverTooltip( From f68e919421f46ec24e9acf21bff5416f39d73a66 Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Mon, 5 Aug 2024 04:08:24 +0200 Subject: [PATCH 334/865] Fix LiveCTPreviewer instantiation --- Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index bbd3b16c..61ccc95c 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -37,7 +37,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase _originalColorTableTexture = new SafeTextureHandle(*_colorTableTexture, true); - if (_originalColorTableTexture == null) + if (_originalColorTableTexture.Texture == null) throw new InvalidOperationException("Material doesn't have a color table"); Width = (int)_originalColorTableTexture.Texture->Width; From 0d1ed6a926ccb593bffa95d78a96b48bd222ecf7 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 09:18:57 +0200 Subject: [PATCH 335/865] No, ImGui, these buttons aren't the same. --- .../Materials/MtrlTab.ColorTable.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index 352681bb..df8485c9 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -91,15 +91,21 @@ public partial class MtrlTab var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { - ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); - ImUtf8.SameLineInner(); - retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + using (ImUtf8.PushId("ClipboardA"u8)) + { + ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); + ImUtf8.SameLineInner(); + retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + } ImGui.SameLine(); CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); columns.Next(); - ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); - ImUtf8.SameLineInner(); - retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + using (ImUtf8.PushId("ClipboardB"u8)) + { + ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); + ImUtf8.SameLineInner(); + retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + } ImGui.SameLine(); CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); } From 1187efa243fe4e645770ca8dc1b80a6159ce0932 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 5 Aug 2024 23:02:34 +0200 Subject: [PATCH 336/865] Reinstate single-row CT highlight --- .../Materials/MtrlTab.ColorTable.cs | 73 +++++++++++-------- .../Materials/MtrlTab.CommonColorTable.cs | 14 +++- .../Materials/MtrlTab.LegacyColorTable.cs | 14 +--- .../Materials/MtrlTab.LivePreview.cs | 28 ++++++- 4 files changed, 86 insertions(+), 43 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index df8485c9..0fa38a5d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -30,11 +30,14 @@ public partial class MtrlTab var buttonWidth = (ImGui.GetContentRegionAvail().X - itemSpacing * 7.0f) * 0.125f; var frameHeight = ImGui.GetFrameHeight(); var highlighterSize = ImUtf8.CalcIconSize(FontAwesomeIcon.Crosshairs) + framePadding * 2.0f; - var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; - var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); using var font = ImRaii.PushFont(UiBuilder.MonoFont); using var alignment = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + + // This depends on the font being pushed for "proper" alignment of the pair indices in the buttons. + var spaceWidth = ImUtf8.CalcTextSize(" "u8).X; + var spacePadding = (int)MathF.Ceiling((highlighterSize.X + framePadding.X + itemInnerSpacing) / spaceWidth); + for (var i = 0; i < ColorTable.NumRows >> 1; i += 8) { for (var j = 0; j < 8; ++j) @@ -72,7 +75,7 @@ public partial class MtrlTab var cursor = ImGui.GetCursorScreenPos(); ImGui.SetCursorScreenPos(rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 0.5f) - highlighterSize.Y * 0.5f }); font.Pop(); - ColorTableHighlightButton(pairIndex, disabled); + ColorTablePairHighlightButton(pairIndex, disabled); font.Push(UiBuilder.MonoFont); ImGui.SetCursorScreenPos(cursor); } @@ -83,6 +86,8 @@ public partial class MtrlTab { var retA = false; var retB = false; + var rowAIdx = _colorTableSelectedPair << 1; + var rowBIdx = rowAIdx | 1; var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; @@ -91,23 +96,15 @@ public partial class MtrlTab var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) { - using (ImUtf8.PushId("ClipboardA"u8)) + using (ImUtf8.PushId("RowHeaderA"u8)) { - ColorTableCopyClipboardButton(_colorTableSelectedPair << 1); - ImUtf8.SameLineInner(); - retA |= ColorTablePasteFromClipboardButton(_colorTableSelectedPair << 1, disabled); + retA |= DrawRowHeader(rowAIdx, disabled); } - ImGui.SameLine(); - CenteredTextInRest($"Row {_colorTableSelectedPair + 1}A"); columns.Next(); - using (ImUtf8.PushId("ClipboardB"u8)) + using (ImUtf8.PushId("RowHeaderB"u8)) { - ColorTableCopyClipboardButton((_colorTableSelectedPair << 1) | 1); - ImUtf8.SameLineInner(); - retB |= ColorTablePasteFromClipboardButton((_colorTableSelectedPair << 1) | 1, disabled); + retB |= DrawRowHeader(rowBIdx, disabled); } - ImGui.SameLine(); - CenteredTextInRest($"Row {_colorTableSelectedPair + 1}B"); } DrawHeader(" Colors"u8); @@ -116,13 +113,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("ColorsA"u8)) { - retA |= DrawColors(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawColors(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("ColorsB"u8)) { - retB |= DrawColors(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawColors(table, dyeTable, dyePackB, rowBIdx); } } @@ -132,13 +129,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("PbrA"u8)) { - retA |= DrawPbr(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawPbr(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("PbrB"u8)) { - retB |= DrawPbr(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawPbr(table, dyeTable, dyePackB, rowBIdx); } } @@ -148,13 +145,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("SheenA"u8)) { - retA |= DrawSheen(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawSheen(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("SheenB"u8)) { - retB |= DrawSheen(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawSheen(table, dyeTable, dyePackB, rowBIdx); } } @@ -164,13 +161,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("BlendingA"u8)) { - retA |= DrawBlending(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawBlending(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("BlendingB"u8)) { - retB |= DrawBlending(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawBlending(table, dyeTable, dyePackB, rowBIdx); } } @@ -180,13 +177,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("TemplateA"u8)) { - retA |= DrawTemplate(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawTemplate(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("TemplateB"u8)) { - retB |= DrawTemplate(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawTemplate(table, dyeTable, dyePackB, rowBIdx); } } @@ -197,13 +194,13 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("DyeA"u8)) { - retA |= DrawDye(dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawDye(dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("DyeB"u8)) { - retB |= DrawDye(dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawDye(dyeTable, dyePackB, rowBIdx); } } @@ -213,20 +210,20 @@ public partial class MtrlTab using var dis = ImRaii.Disabled(disabled); using (ImUtf8.PushId("FurtherA"u8)) { - retA |= DrawFurther(table, dyeTable, dyePackA, _colorTableSelectedPair << 1); + retA |= DrawFurther(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); using (ImUtf8.PushId("FurtherB"u8)) { - retB |= DrawFurther(table, dyeTable, dyePackB, (_colorTableSelectedPair << 1) | 1); + retB |= DrawFurther(table, dyeTable, dyePackB, rowBIdx); } } if (retA) - UpdateColorTableRowPreview(_colorTableSelectedPair << 1); + UpdateColorTableRowPreview(rowAIdx); if (retB) - UpdateColorTableRowPreview((_colorTableSelectedPair << 1) | 1); + UpdateColorTableRowPreview(rowBIdx); return retA | retB; } @@ -239,6 +236,20 @@ public partial class MtrlTab ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); } + private bool DrawRowHeader(int rowIdx, bool disabled) + { + ColorTableCopyClipboardButton(rowIdx); + ImUtf8.SameLineInner(); + var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); + ImUtf8.SameLineInner(); + ColorTableRowHighlightButton(rowIdx, disabled); + + ImGui.SameLine(); + CenteredTextInRest($"Row {(rowIdx >> 1) + 1}{"AB"[rowIdx & 1]}"); + + return ret; + } + private static bool DrawColors(ColorTable table, ColorDyeTable? dyeTable, DyePack? dyePack, int rowIdx) { var dyeOffset = ImGui.GetContentRegionAvail().X diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 09c8ea61..38f02100 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -228,7 +228,7 @@ public partial class MtrlTab } } - private void ColorTableHighlightButton(int pairIdx, bool disabled) + private void ColorTablePairHighlightButton(int pairIdx, bool disabled) { ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, "Highlight this pair of rows on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, @@ -240,6 +240,18 @@ public partial class MtrlTab CancelColorTableHighlight(); } + private void ColorTableRowHighlightButton(int rowIdx, bool disabled) + { + ImUtf8.IconButton(FontAwesomeIcon.Crosshairs, + "Highlight this row on your character, if possible.\n\nHighlight colors can be configured in Penumbra's settings."u8, + ImGui.GetFrameHeight() * Vector2.One, disabled || _colorTablePreviewers.Count == 0); + + if (ImGui.IsItemHovered()) + HighlightColorTableRow(rowIdx); + else if (_highlightedColorTableRow == rowIdx) + CancelColorTableHighlight(); + } + private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) { var style = ImGui.GetStyle(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index 0ff2b01f..f21d86a9 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -103,11 +103,8 @@ public partial class MtrlTab ColorTableCopyClipboardButton(rowIdx); ImUtf8.SameLineInner(); var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); - if ((rowIdx & 1) == 0) - { - ImUtf8.SameLineInner(); - ColorTableHighlightButton(rowIdx >> 1, disabled); - } + ImUtf8.SameLineInner(); + ColorTableRowHighlightButton(rowIdx, disabled); ImGui.TableNextColumn(); using (ImRaii.PushFont(UiBuilder.MonoFont)) @@ -213,11 +210,8 @@ public partial class MtrlTab ColorTableCopyClipboardButton(rowIdx); ImUtf8.SameLineInner(); var ret = ColorTablePasteFromClipboardButton(rowIdx, disabled); - if ((rowIdx & 1) == 0) - { - ImUtf8.SameLineInner(); - ColorTableHighlightButton(rowIdx >> 1, disabled); - } + ImUtf8.SameLineInner(); + ColorTableRowHighlightButton(rowIdx, disabled); ImGui.TableNextColumn(); using (ImRaii.PushFont(UiBuilder.MonoFont)) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 6089f2d5..01a40980 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -14,6 +14,7 @@ public partial class MtrlTab { private readonly List _materialPreviewers = new(4); private readonly List _colorTablePreviewers = new(4); + private int _highlightedColorTableRow = -1; private int _highlightedColorTablePair = -1; private readonly Stopwatch _highlightTime = new(); @@ -168,13 +169,35 @@ public partial class MtrlTab } } + private void HighlightColorTableRow(int rowIdx) + { + var oldRowIdx = _highlightedColorTableRow; + + if (_highlightedColorTableRow != rowIdx) + { + _highlightedColorTableRow = rowIdx; + _highlightTime.Restart(); + } + + if (oldRowIdx >= 0) + UpdateColorTableRowPreview(oldRowIdx); + + if (rowIdx >= 0) + UpdateColorTableRowPreview(rowIdx); + } + private void CancelColorTableHighlight() { + var rowIdx = _highlightedColorTableRow; var pairIdx = _highlightedColorTablePair; + _highlightedColorTableRow = -1; _highlightedColorTablePair = -1; _highlightTime.Reset(); + if (rowIdx >= 0) + UpdateColorTableRowPreview(rowIdx); + if (pairIdx >= 0) { UpdateColorTableRowPreview(pairIdx << 1); @@ -214,7 +237,7 @@ public partial class MtrlTab } } - if (_highlightedColorTablePair << 1 == rowIdx) + if (_highlightedColorTablePair << 1 == rowIdx || _highlightedColorTableRow == rowIdx) ApplyHighlight(ref row, ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); else if (((_highlightedColorTablePair << 1) | 1) == rowIdx) ApplyHighlight(ref row, ColorId.InGameHighlight2, (float)_highlightTime.Elapsed.TotalSeconds); @@ -247,6 +270,9 @@ public partial class MtrlTab rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); } + if (_highlightedColorTableRow >= 0) + ApplyHighlight(ref rows[_highlightedColorTableRow], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); + if (_highlightedColorTablePair >= 0) { ApplyHighlight(ref rows[_highlightedColorTablePair << 1], ColorId.InGameHighlight, (float)_highlightTime.Elapsed.TotalSeconds); From 2bf08c8c899dcb2111098affc6902b60ffcca9c6 Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Tue, 6 Aug 2024 13:05:21 +0200 Subject: [PATCH 337/865] Fix dye template combo (aka "git gud") --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index df8485c9..13a36c71 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -595,7 +595,7 @@ public partial class MtrlTab if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dye.Template = _stainService.LegacyTemplateCombo.CurrentSelection; + dye.Template = _stainService.GudTemplateCombo.CurrentSelection; ret = true; } From df58ac7e9248fdf3fcf465c1a6c2880577331d79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 7 Aug 2024 15:44:21 +0200 Subject: [PATCH 338/865] Fix ref. --- Penumbra/Communication/MtrlShpkLoaded.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Communication/MtrlShpkLoaded.cs b/Penumbra/Communication/MtrlShpkLoaded.cs index 9d3597a8..2b286bb9 100644 --- a/Penumbra/Communication/MtrlShpkLoaded.cs +++ b/Penumbra/Communication/MtrlShpkLoaded.cs @@ -10,7 +10,7 @@ public sealed class MtrlShpkLoaded() : EventWrapper + /// ShaderReplacementFixer = 0, } } From fe4a046cc99b7ede7777c233df4089992931e914 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 7 Aug 2024 16:37:58 +0200 Subject: [PATCH 339/865] Make ChatWarningService part of the MessageService. --- OtterGui | 2 +- .../Processing/ShpkPathPreProcessor.cs | 27 +++++---- Penumbra/Services/MessageService.cs | 16 ++++++ Penumbra/UI/ChatWarningService.cs | 56 ------------------- 4 files changed, 32 insertions(+), 69 deletions(-) delete mode 100644 Penumbra/UI/ChatWarningService.cs diff --git a/OtterGui b/OtterGui index c53955cb..d9486ae5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c53955cb6199dd418c5a9538d3251ac5942e7067 +Subproject commit d9486ae54b5a4b61cf74f79ed27daa659eb1ce5b diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 2c6f6901..96d9daff 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -4,23 +4,26 @@ using Penumbra.Collections; using Penumbra.GameData.Files; using Penumbra.GameData.Files.Utility; using Penumbra.Interop.Hooks.ResourceLoading; +using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.String; using Penumbra.String.Classes; -using Penumbra.UI; namespace Penumbra.Interop.Processing; /// /// Path pre-processor for shader packages that reverts redirects to known invalid files, as bad ShPks can crash the game. /// -public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, ChatWarningService chatWarningService) : IPathPreProcessor +public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, MessageService messager, ModManager modManager) + : IPathPreProcessor { public ResourceType Type => ResourceType.Shpk; - public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, FullPath? resolved) + public unsafe FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath originalGamePath, bool nonDefault, + FullPath? resolved) { - chatWarningService.CleanLastFileWarnings(false); + messager.CleanTaggedMessages(false); if (!resolved.HasValue) return null; @@ -31,7 +34,8 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, return resolvedPath; // If the ShPk is already loaded, it means that it already passed the sanity check. - var existingResource = resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32)); + var existingResource = + resourceManager.FindResource(ResourceCategory.Shader, ResourceType.Shpk, unchecked((uint)resolvedPath.InternalName.Crc32)); if (existingResource != null) return resolvedPath; @@ -39,8 +43,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, if (checkResult == SanityCheckResult.Success) return resolvedPath; - Penumbra.Log.Warning($"Refusing to honor file redirection because of failed sanity check (result: {checkResult}). Original path: {originalGamePath} Redirected path: {resolvedPath}"); - chatWarningService.PrintFileWarning(resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult)); + messager.PrintFileWarning(modManager, resolvedPath.FullName, originalGamePath, WarningMessageComplement(checkResult)); return null; } @@ -49,8 +52,8 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, { try { - using var file = MmioMemoryManager.CreateFromFile(path); - var bytes = file.GetSpan(); + using var file = MmioMemoryManager.CreateFromFile(path); + var bytes = file.GetSpan(); return ShpkFile.FastIsLegacy(bytes) ? SanityCheckResult.Legacy @@ -69,9 +72,9 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, private static string WarningMessageComplement(SanityCheckResult result) => result switch { - SanityCheckResult.IoError => "cannot read the modded file.", - SanityCheckResult.NotFound => "the modded file does not exist.", - SanityCheckResult.Legacy => "this mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", + SanityCheckResult.IoError => "Cannot read the modded file.", + SanityCheckResult.NotFound => "The modded file does not exist.", + SanityCheckResult.Legacy => "This mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", _ => string.Empty, }; diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index 08118483..a35a67f1 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -2,10 +2,14 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; using OtterGui.Log; using OtterGui.Services; +using Penumbra.Mods.Manager; +using Penumbra.String.Classes; +using Notification = OtterGui.Classes.Notification; namespace Penumbra.Services; @@ -38,4 +42,16 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti Message = payload, }); } + + public void PrintFileWarning(ModManager modManager, string fullPath, Utf8GamePath originalGamePath, string messageComplement) + { + // Don't warn for files managed by other plugins, or files we aren't sure about. + if (!modManager.TryIdentifyPath(fullPath, out var mod, out _)) + return; + + AddTaggedMessage($"{fullPath}.{messageComplement}", + new Notification( + $"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}", + NotificationType.Warning, 10000)); + } } diff --git a/Penumbra/UI/ChatWarningService.cs b/Penumbra/UI/ChatWarningService.cs deleted file mode 100644 index 84ede2fb..00000000 --- a/Penumbra/UI/ChatWarningService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Dalamud.Plugin.Services; -using OtterGui.Services; -using Penumbra.Mods.Manager; -using Penumbra.String.Classes; - -namespace Penumbra.UI; - -public sealed class ChatWarningService(IChatGui chatGui, IClientState clientState, ModManager modManager) : IUiService -{ - private readonly Dictionary _lastFileWarnings = []; - private int _lastFileWarningsCleanCounter; - - private const int LastFileWarningsCleanCycle = 100; - private static readonly TimeSpan LastFileWarningsMaxAge = new(1, 0, 0); - - public void CleanLastFileWarnings(bool force) - { - if (!force) - { - _lastFileWarningsCleanCounter = (_lastFileWarningsCleanCounter + 1) % LastFileWarningsCleanCycle; - if (_lastFileWarningsCleanCounter != 0) - return; - } - - var expiredDate = DateTime.Now - LastFileWarningsMaxAge; - var toRemove = new HashSet(); - foreach (var (key, value) in _lastFileWarnings) - { - if (value.Item1 <= expiredDate) - toRemove.Add(key); - } - foreach (var key in toRemove) - _lastFileWarnings.Remove(key); - } - - public void PrintFileWarning(string fullPath, Utf8GamePath originalGamePath, string messageComplement) - { - CleanLastFileWarnings(true); - - // Don't warn twice for the same file within a certain time interval unless the reason changed. - if (_lastFileWarnings.TryGetValue(fullPath, out var lastWarning) && lastWarning.Item2 == messageComplement) - return; - - // Don't warn for files managed by other plugins, or files we aren't sure about. - if (!modManager.TryIdentifyPath(fullPath, out var mod, out _)) - return; - - // Don't warn if there's no local player (as an approximation of no chat), so as not to trigger the cooldown. - if (clientState.LocalPlayer == null) - return; - - // The wording is an allusion to tar's "Cowardly refusing to create an empty archive" - chatGui.PrintError($"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ": " : ".")}{messageComplement}", "Penumbra"); - _lastFileWarnings[fullPath] = (DateTime.Now, messageComplement); - } -} From d630a3dff42295b972a0c1d864468d4d384edb9b Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 7 Aug 2024 14:47:58 +0000 Subject: [PATCH 340/865] [CI] Updating repo.json for testing_1.2.0.22 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 10a08fa1..3de0c5c8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.21", + "TestingAssemblyVersion": "1.2.0.22", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.21/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.22/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From fb58a9c27194d2107cd926d3a31f5a8d4600a1d4 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 8 Aug 2024 23:19:18 +0200 Subject: [PATCH 341/865] Add/improve ShaderReplacementFixer hooks --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../PostProcessing/HumanSetupScalingHook.cs | 55 ++++ .../PostProcessing/PreBoneDeformerReplacer.cs | 49 +-- .../PostProcessing/ShaderReplacementFixer.cs | 284 ++++++++++++++++-- Penumbra/Interop/Services/CharacterUtility.cs | 32 +- Penumbra/Interop/Services/ModelRenderer.cs | 44 +-- .../Interop/Structs/CharacterUtilityData.cs | 26 +- .../Interop/Structs/ModelRendererStructs.cs | 35 +++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 62 ++-- 10 files changed, 478 insertions(+), 113 deletions(-) create mode 100644 Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs create mode 100644 Penumbra/Interop/Structs/ModelRendererStructs.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ac9d9c78..2fd5aa44 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ac9d9c78ae0025489b80ce2e798cdaacb0b43947 +Subproject commit 2fd5aa44056a906df90c9a826d1d17f6fdafebff diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 0bc55dc5..a1dd374f 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -80,6 +80,8 @@ public class HookOverrides public bool HumanCreateDeformer; public bool HumanOnRenderMaterial; public bool ModelRendererOnRenderMaterial; + public bool ModelRendererUnkFunc; + public bool PrepareColorTable; } public struct ResourceLoadingHooks diff --git a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs new file mode 100644 index 00000000..5783c099 --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +// TODO: "SetupScaling" does not seem to only set up scaling -> find a better name? +public unsafe class HumanSetupScalingHook : FastHook +{ + private const int ReplacementCapacity = 2; + + public event EventDelegate? SetupReplacements; + + public HumanSetupScalingHook(HookManager hooks, CharacterBaseVTables vTables) + { + Task = hooks.CreateHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, + !HookOverrides.Instance.PostProcessing.HumanSetupScaling); + } + + private void Detour(CharacterBase* drawObject, uint slotIndex) + { + Span replacements = stackalloc Replacement[ReplacementCapacity]; + var numReplacements = 0; + IDisposable? pbdDisposable = null; + object? shpkLock = null; + var releaseLock = false; + + try + { + SetupReplacements?.Invoke(drawObject, slotIndex, replacements, ref numReplacements, ref pbdDisposable, ref shpkLock); + if (shpkLock != null) + { + Monitor.Enter(shpkLock); + releaseLock = true; + } + for (var i = 0; i < numReplacements; ++i) + *(nint*)replacements[i].AddressToReplace = replacements[i].ValueToSet; + Task.Result.Original(drawObject, slotIndex); + } + finally + { + for (var i = numReplacements; i-- > 0;) + *(nint*)replacements[i].AddressToReplace = replacements[i].ValueToRestore; + if (releaseLock) + Monitor.Exit(shpkLock!); + pbdDisposable?.Dispose(); + } + } + + public delegate void Delegate(CharacterBase* drawObject, uint slotIndex); + + public delegate void EventDelegate(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, + ref IDisposable? pbdDisposable, ref object? shpkLock); + + public readonly record struct Replacement(nint AddressToReplace, nint ValueToSet, nint ValueToRestore); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 1aa09d7f..9273a2cb 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -18,27 +18,26 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public static readonly Utf8GamePath PreBoneDeformerPath = Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; - // Approximate name guesses. - private delegate void CharacterBaseSetupScalingDelegate(CharacterBase* drawObject, uint slotIndex); - private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + // Approximate name guess. + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); - private readonly Hook _humanSetupScalingHook; private readonly Hook _humanCreateDeformerHook; - private readonly CharacterUtility _utility; - private readonly CollectionResolver _collectionResolver; - private readonly ResourceLoader _resourceLoader; - private readonly IFramework _framework; + private readonly CharacterUtility _utility; + private readonly CollectionResolver _collectionResolver; + private readonly ResourceLoader _resourceLoader; + private readonly IFramework _framework; + private readonly HumanSetupScalingHook _humanSetupScalingHook; public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, - HookManager hooks, IFramework framework, CharacterBaseVTables vTables) + HookManager hooks, IFramework framework, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = hooks.CreateHook("HumanSetupScaling", vTables.HumanVTable[58], SetupScaling, - !HookOverrides.Instance.PostProcessing.HumanSetupScaling).Result; + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = humanSetupScalingHook; + _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } @@ -46,7 +45,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public void Dispose() { _humanCreateDeformerHook.Dispose(); - _humanSetupScalingHook.Dispose(); + _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; } private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) @@ -58,22 +57,24 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } - private void SetupScaling(CharacterBase* drawObject, uint slotIndex) + private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( - $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupScaling)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHSSReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); - using var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); + var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { - if (!preBoneDeformer.IsInvalid) - _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)preBoneDeformer.ResourceHandle; - _humanSetupScalingHook.Original(drawObject, slotIndex); + pbdDisposable = preBoneDeformer; + replacements[numReplacements++] = new((nint)(&_utility.Address->HumanPbdResource), (nint)preBoneDeformer.ResourceHandle, + _utility.DefaultHumanPbdResource); } - finally + catch { - _utility.Address->HumanPbdResource = (Structs.ResourceHandle*)_utility.DefaultHumanPbdResource; + preBoneDeformer.Dispose(); + throw; } } diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index d02e18bb..20db7e25 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -1,4 +1,5 @@ using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; @@ -7,6 +8,8 @@ using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; +using Penumbra.Interop.Services; +using Penumbra.Interop.Structs; using Penumbra.Services; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; @@ -19,6 +22,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public static ReadOnlySpan SkinShpkName => "skin.shpk"u8; + public static ReadOnlySpan CharacterStockingsShpkName + => "characterstockings.shpk"u8; + + public static ReadOnlySpan CharacterLegacyShpkName + => "characterlegacy.shpk"u8; + public static ReadOnlySpan IrisShpkName => "iris.shpk"u8; @@ -42,16 +51,26 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private delegate nint ModelRendererOnRenderMaterialDelegate(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex); + private delegate void ModelRendererUnkFuncDelegate(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, + uint unk3, uint unk4, uint unk5); + private readonly Hook _humanOnRenderMaterialHook; private readonly Hook _modelRendererOnRenderMaterialHook; + private readonly Hook _modelRendererUnkFuncHook; + + private readonly Hook _prepareColorTableHook; + private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; private readonly CharacterUtility _utility; private readonly ModelRenderer _modelRenderer; + private readonly HumanSetupScalingHook _humanSetupScalingHook; private readonly ModdedShaderPackageState _skinState; + private readonly ModdedShaderPackageState _characterStockingsState; + private readonly ModdedShaderPackageState _characterLegacyState; private readonly ModdedShaderPackageState _irisState; private readonly ModdedShaderPackageState _characterGlassState; private readonly ModdedShaderPackageState _characterTransparencyState; @@ -64,6 +83,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic public uint ModdedSkinShpkCount => _skinState.MaterialCount; + public uint ModdedCharacterStockingsShpkCount + => _characterStockingsState.MaterialCount; + + public uint ModdedCharacterLegacyShpkCount + => _characterLegacyState.MaterialCount; + public uint ModdedIrisShpkCount => _irisState.MaterialCount; @@ -83,16 +108,23 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _hairMaskState.MaterialCount; public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, - CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables) + CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; _utility = utility; _modelRenderer = modelRenderer; _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + _characterStockingsState = new ModdedShaderPackageState( + () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterStockingsShpkResource); + _characterLegacyState = new ModdedShaderPackageState( + () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterLegacyShpkResource); _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, () => _modelRenderer.DefaultCharacterGlassShaderPackage); @@ -105,33 +137,50 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState = new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", Sigs.ModelRendererOnRenderMaterial, ModelRendererOnRenderMaterialDetour, !HookOverrides.Instance.PostProcessing.ModelRendererOnRenderMaterial).Result; + _modelRendererUnkFuncHook = hooks.CreateHook("ModelRenderer.UnkFunc", + Sigs.ModelRendererUnkFunc, ModelRendererUnkFuncDetour, + !HookOverrides.Instance.PostProcessing.ModelRendererUnkFunc).Result; + _prepareColorTableHook = hooks.CreateHook("MaterialResourceHandle.PrepareColorTable", + Sigs.PrepareColorSet, PrepareColorTableDetour, + !HookOverrides.Instance.PostProcessing.PrepareColorTable).Result; + _communicator.MtrlLoaded.Subscribe(OnMtrlLoaded, MtrlLoaded.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); } public void Dispose() { + _prepareColorTableHook.Dispose(); + _modelRendererUnkFuncHook.Dispose(); _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); + _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); + _hairMaskState.ClearMaterials(); _characterOcclusionState.ClearMaterials(); _characterTattooState.ClearMaterials(); _characterTransparencyState.ClearMaterials(); _characterGlassState.ClearMaterials(); _irisState.ClearMaterials(); + _characterLegacyState.ClearMaterials(); + _characterStockingsState.ClearMaterials(); _skinState.ClearMaterials(); } - public (ulong Skin, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong CharacterTattoo, ulong CharacterOcclusion, ulong - HairMask) GetAndResetSlowPathCallDeltas() + public (ulong Skin, ulong CharacterStockings, ulong CharacterLegacy, ulong Iris, ulong CharacterGlass, ulong CharacterTransparency, ulong + CharacterTattoo, ulong CharacterOcclusion, ulong HairMask) GetAndResetSlowPathCallDeltas() => (_skinState.GetAndResetSlowPathCallDelta(), + _characterStockingsState.GetAndResetSlowPathCallDelta(), + _characterLegacyState.GetAndResetSlowPathCallDelta(), _irisState.GetAndResetSlowPathCallDelta(), _characterGlassState.GetAndResetSlowPathCallDelta(), _characterTransparencyState.GetAndResetSlowPathCallDelta(), @@ -155,7 +204,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return; var shpkName = mtrl->ShpkNameSpan; - var shpkState = GetStateForHuman(shpkName) ?? GetStateForModelRenderer(shpkName); + var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) + ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); if (shpkState != null && shpk != shpkState.DefaultShaderPackage) shpkState.TryAddMaterial(mtrlResourceHandle); @@ -164,6 +214,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) { _skinState.TryRemoveMaterial(handle); + _characterStockingsState.TryRemoveMaterial(handle); + _characterLegacyState.TryRemoveMaterial(handle); _irisState.TryRemoveMaterial(handle); _characterGlassState.TryRemoveMaterial(handle); _characterTransparencyState.TryRemoveMaterial(handle); @@ -172,10 +224,25 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState.TryRemoveMaterial(handle); } - private ModdedShaderPackageState? GetStateForHuman(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHuman(mtrlResource->ShpkNameSpan); + private ModdedShaderPackageState? GetStateForHumanSetup(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); - private ModdedShaderPackageState? GetStateForHuman(ReadOnlySpan shpkName) + private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) + { + if (CharacterStockingsShpkName.SequenceEqual(shpkName)) + return _characterStockingsState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForHumanSetup() + => _characterStockingsState.MaterialCount; + + private ModdedShaderPackageState? GetStateForHumanRender(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) { if (SkinShpkName.SequenceEqual(shpkName)) return _skinState; @@ -184,17 +251,14 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private uint GetTotalMaterialCountForHuman() + private uint GetTotalMaterialCountForHumanRender() => _skinState.MaterialCount; - private ModdedShaderPackageState? GetStateForModelRenderer(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRenderer(mtrlResource->ShpkNameSpan); + private ModdedShaderPackageState? GetStateForModelRendererRender(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkNameSpan); - private ModdedShaderPackageState? GetStateForModelRenderer(ReadOnlySpan shpkName) + private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan shpkName) { - if (IrisShpkName.SequenceEqual(shpkName)) - return _irisState; - if (CharacterGlassShpkName.SequenceEqual(shpkName)) return _characterGlassState; @@ -204,9 +268,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (CharacterTattooShpkName.SequenceEqual(shpkName)) return _characterTattooState; - if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) - return _characterOcclusionState; - if (HairMaskShpkName.SequenceEqual(shpkName)) return _hairMaskState; @@ -214,24 +275,93 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private uint GetTotalMaterialCountForModelRenderer() - => _irisState.MaterialCount - + _characterGlassState.MaterialCount + private uint GetTotalMaterialCountForModelRendererRender() + => _characterGlassState.MaterialCount + _characterTransparencyState.MaterialCount + _characterTattooState.MaterialCount - + _characterOcclusionState.MaterialCount + _hairMaskState.MaterialCount; + private ModdedShaderPackageState? GetStateForModelRendererUnk(MaterialResourceHandle* mtrlResource) + => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkNameSpan); + + private ModdedShaderPackageState? GetStateForModelRendererUnk(ReadOnlySpan shpkName) + { + if (IrisShpkName.SequenceEqual(shpkName)) + return _irisState; + + if (CharacterOcclusionShpkName.SequenceEqual(shpkName)) + return _characterOcclusionState; + + if (CharacterStockingsShpkName.SequenceEqual(shpkName)) + return _characterStockingsState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForModelRendererUnk() + => _irisState.MaterialCount + + _characterOcclusionState.MaterialCount + + _characterStockingsState.MaterialCount; + + private ModdedShaderPackageState? GetStateForColorTable(ReadOnlySpan shpkName) + { + if (CharacterLegacyShpkName.SequenceEqual(shpkName)) + return _characterLegacyState; + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private uint GetTotalMaterialCountForColorTable() + => _characterLegacyState.MaterialCount; + + private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) + { + // If we don't have any on-screen instances of modded characterstockings.shpk, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForHumanSetup() == 0) + return; + + var model = drawObject->Models[slotIndex]; + if (model == null) + return; + MaterialResourceHandle* mtrlResource = null; + ModdedShaderPackageState? shpkState = null; + foreach (var material in model->MaterialsSpan) + { + if (material.Value == null) + continue; + + mtrlResource = material.Value->MaterialResourceHandle; + shpkState = GetStateForHumanSetup(mtrlResource); + // Despite this function being called with what designates a model (and therefore potentially many materials), + // we currently don't need to handle more than one modded ShPk. + if (shpkState != null) + break; + } + if (shpkState == null || shpkState.MaterialCount == 0) + return; + + shpkState.IncrementSlowPathCallDelta(); + + // This is less performance-critical than the others, as this is called by the game only on draw object creation and slot update. + // There are still thread safety concerns as it might be called in other threads by plugins. + shpkLock = shpkState; + replacements[numReplacements++] = new((nint)shpkState.ShaderPackageReference, (nint)mtrlResource->ShaderPackageResourceHandle, + (nint)shpkState.DefaultShaderPackage); + } + private nint OnRenderHumanMaterial(CharacterBase* human, CSModelRenderer.OnRenderMaterialParams* param) { // If we don't have any on-screen instances of modded skin.shpk, we don't need the slow path at all. - if (!Enabled || GetTotalMaterialCountForHuman() == 0) + if (!Enabled || GetTotalMaterialCountForHumanRender() == 0) return _humanOnRenderMaterialHook.Original(human, param); var material = param->Model->Materials[param->MaterialIndex]; var mtrlResource = material->MaterialResourceHandle; - var shpkState = GetStateForHuman(mtrlResource); - if (shpkState == null) + var shpkState = GetStateForHumanRender(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) return _humanOnRenderMaterialHook.Original(human, param); shpkState.IncrementSlowPathCallDelta(); @@ -259,18 +389,18 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private nint ModelRendererOnRenderMaterialDetour(CSModelRenderer* modelRenderer, ushort* outFlags, CSModelRenderer.OnRenderModelParams* param, Material* material, uint materialIndex) { - // If we don't have any on-screen instances of modded characterglass.shpk, we don't need the slow path at all. - if (!Enabled || GetTotalMaterialCountForModelRenderer() == 0) + // If we don't have any on-screen instances of modded characterglass.shpk or others, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForModelRendererRender() == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); var mtrlResource = material->MaterialResourceHandle; - var shpkState = GetStateForModelRenderer(mtrlResource); - if (shpkState == null) + var shpkState = GetStateForModelRendererRender(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); shpkState.IncrementSlowPathCallDelta(); - // Same performance considerations as above. + // Same performance considerations as OnRenderHumanMaterial. lock (shpkState) { var shpkReference = shpkState.ShaderPackageReference; @@ -286,6 +416,102 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } } + private void ModelRendererUnkFuncDetour(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, uint unk3, + uint unk4, uint unk5) + { + // If we don't have any on-screen instances of modded iris.shpk or others, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForModelRendererUnk() == 0) + { + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + return; + } + + var mtrlResource = GetMaterialResourceHandle(unkPayload); + var shpkState = GetStateForModelRendererUnk(mtrlResource); + if (shpkState == null || shpkState.MaterialCount == 0) + { + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + return; + } + + shpkState.IncrementSlowPathCallDelta(); + + // Same performance considerations as OnRenderHumanMaterial. + lock (shpkState) + { + var shpkReference = shpkState.ShaderPackageReference; + try + { + *shpkReference = mtrlResource->ShaderPackageResourceHandle; + _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); + } + finally + { + *shpkReference = shpkState.DefaultShaderPackage; + } + } + } + + private MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) + { + // TODO ClientStructs-ify + 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) + return null; + + var mtrlResource = material->MaterialResourceHandle; + if (mtrlResource == null) + return null; + + if (mtrlResource->ShaderPackageResourceHandle == null) + { + Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); + return null; + } + + if (mtrlResource->ShaderPackageResourceHandle->ShaderPackage != unkPayload->ShaderWrapper->ShaderPackage) + { + Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); + return null; + } + + return mtrlResource; + } + + private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) + { + // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. + if (!Enabled || GetTotalMaterialCountForColorTable() == 0) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + var material = thisPtr->Material; + if (material == null) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + var shpkState = GetStateForColorTable(thisPtr->ShpkNameSpan); + if (shpkState == null || shpkState.MaterialCount == 0) + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + + shpkState.IncrementSlowPathCallDelta(); + + // Same performance considerations as HumanSetupScalingDetour. + lock (shpkState) + { + var shpkReference = shpkState.ShaderPackageReference; + try + { + *shpkReference = thisPtr->ShaderPackageResourceHandle; + return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); + } + finally + { + *shpkReference = shpkState.DefaultShaderPackage; + } + } + } + private sealed class ModdedShaderPackageState(ShaderPackageReferenceGetter referenceGetter, DefaultShaderPackageGetter defaultGetter) { // MaterialResourceHandle set diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 532dc823..4ab156a9 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -28,10 +28,12 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService public bool Ready { get; private set; } public event Action LoadingFinished; - public nint DefaultHumanPbdResource { get; private set; } - public nint DefaultTransparentResource { get; private set; } - public nint DefaultDecalResource { get; private set; } - public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultHumanPbdResource { get; private set; } + public nint DefaultTransparentResource { get; private set; } + public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultCharacterStockingsShpkResource { get; private set; } + public nint DefaultCharacterLegacyShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -108,6 +110,18 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService anyMissing |= DefaultSkinShpkResource == nint.Zero; } + if (DefaultCharacterStockingsShpkResource == nint.Zero) + { + DefaultCharacterStockingsShpkResource = (nint)Address->CharacterStockingsShpkResource; + anyMissing |= DefaultCharacterStockingsShpkResource == nint.Zero; + } + + if (DefaultCharacterLegacyShpkResource == nint.Zero) + { + DefaultCharacterLegacyShpkResource = (nint)Address->CharacterLegacyShpkResource; + anyMissing |= DefaultCharacterLegacyShpkResource == nint.Zero; + } + if (anyMissing) return; @@ -122,10 +136,12 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService if (!Ready) return; - Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; - Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; - Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; - Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; + Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource; + Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; + Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; + Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; + Address->CharacterStockingsShpkResource = (ResourceHandle*)DefaultCharacterStockingsShpkResource; + Address->CharacterLegacyShpkResource = (ResourceHandle*)DefaultCharacterLegacyShpkResource; } public void Dispose() diff --git a/Penumbra/Interop/Services/ModelRenderer.cs b/Penumbra/Interop/Services/ModelRenderer.cs index 10f3977f..5e2cd1fb 100644 --- a/Penumbra/Interop/Services/ModelRenderer.cs +++ b/Penumbra/Interop/Services/ModelRenderer.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using OtterGui.Services; +using ModelRendererData = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; namespace Penumbra.Interop.Services; @@ -9,46 +10,53 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService { public bool Ready { get; private set; } - public ShaderPackageResourceHandle** IrisShaderPackage + public ModelRendererData* Address => Manager.Instance() switch { null => null, - var renderManager => &renderManager->ModelRenderer.IrisShaderPackage, + var renderManager => &renderManager->ModelRenderer, + }; + + public ShaderPackageResourceHandle** IrisShaderPackage + => Address switch + { + null => null, + var data => &data->IrisShaderPackage, }; public ShaderPackageResourceHandle** CharacterGlassShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterGlassShaderPackage, + null => null, + var data => &data->CharacterGlassShaderPackage, }; public ShaderPackageResourceHandle** CharacterTransparencyShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterTransparencyShaderPackage, + null => null, + var data => &data->CharacterTransparencyShaderPackage, }; public ShaderPackageResourceHandle** CharacterTattooShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterTattooShaderPackage, + null => null, + var data => &data->CharacterTattooShaderPackage, }; public ShaderPackageResourceHandle** CharacterOcclusionShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.CharacterOcclusionShaderPackage, + null => null, + var data => &data->CharacterOcclusionShaderPackage, }; public ShaderPackageResourceHandle** HairMaskShaderPackage - => Manager.Instance() switch + => Address switch { - null => null, - var renderManager => &renderManager->ModelRenderer.HairMaskShaderPackage, + null => null, + var data => &data->HairMaskShaderPackage, }; public ShaderPackageResourceHandle* DefaultIrisShaderPackage { get; private set; } @@ -96,7 +104,7 @@ public unsafe class ModelRenderer : IDisposable, IRequiredService if (DefaultCharacterTransparencyShaderPackage == null) { DefaultCharacterTransparencyShaderPackage = *CharacterTransparencyShaderPackage; - anyMissing |= DefaultCharacterTransparencyShaderPackage == null; + anyMissing |= DefaultCharacterTransparencyShaderPackage == null; } if (DefaultCharacterTattooShaderPackage == null) diff --git a/Penumbra/Interop/Structs/CharacterUtilityData.cs b/Penumbra/Interop/Structs/CharacterUtilityData.cs index 7595353f..8543466d 100644 --- a/Penumbra/Interop/Structs/CharacterUtilityData.cs +++ b/Penumbra/Interop/Structs/CharacterUtilityData.cs @@ -5,15 +5,17 @@ namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] public unsafe struct CharacterUtilityData { - public const int IndexHumanPbd = 63; - public const int IndexTransparentTex = 79; - public const int IndexDecalTex = 80; - public const int IndexTileOrbArrayTex = 81; - public const int IndexTileNormArrayTex = 82; - public const int IndexSkinShpk = 83; - public const int IndexGudStm = 94; - public const int IndexLegacyStm = 95; - public const int IndexSphereDArrayTex = 96; + public const int IndexHumanPbd = 63; + public const int IndexTransparentTex = 79; + public const int IndexDecalTex = 80; + public const int IndexTileOrbArrayTex = 81; + public const int IndexTileNormArrayTex = 82; + public const int IndexSkinShpk = 83; + public const int IndexCharacterStockingsShpk = 84; + public const int IndexCharacterLegacyShpk = 85; + public const int IndexGudStm = 94; + public const int IndexLegacyStm = 95; + public const int IndexSphereDArrayTex = 96; public static readonly MetaIndex[] EqdpIndices = Enum.GetNames() .Zip(Enum.GetValues()) @@ -111,6 +113,12 @@ public unsafe struct CharacterUtilityData [FieldOffset(8 + IndexSkinShpk * 8)] public ResourceHandle* SkinShpkResource; + [FieldOffset(8 + IndexCharacterStockingsShpk * 8)] + public ResourceHandle* CharacterStockingsShpkResource; + + [FieldOffset(8 + IndexCharacterLegacyShpk * 8)] + public ResourceHandle* CharacterLegacyShpkResource; + [FieldOffset(8 + IndexGudStm * 8)] public ResourceHandle* GudStmResource; diff --git a/Penumbra/Interop/Structs/ModelRendererStructs.cs b/Penumbra/Interop/Structs/ModelRendererStructs.cs new file mode 100644 index 00000000..551a32e3 --- /dev/null +++ b/Penumbra/Interop/Structs/ModelRendererStructs.cs @@ -0,0 +1,35 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; + +namespace Penumbra.Interop.Structs; + +public static unsafe class ModelRendererStructs +{ + [StructLayout(LayoutKind.Explicit, Size = 0x28)] + public struct UnkShaderWrapper + { + [FieldOffset(0)] + public void* Vtbl; + + [FieldOffset(8)] + public ShaderPackage* ShaderPackage; + } + + // Unknown size, this is allocated on FUN_1404446c0's stack (E8 ?? ?? ?? ?? FF C3 41 3B DE 72 ?? 48 C7 85) + [StructLayout(LayoutKind.Explicit)] + public struct UnkPayload + { + [FieldOffset(0)] + public ModelRenderer.OnRenderModelParams* Params; + + [FieldOffset(8)] + public ModelResourceHandle* ModelResourceHandle; + + [FieldOffset(0x10)] + public UnkShaderWrapper* ShaderWrapper; + + [FieldOffset(0x1C)] + public ushort UnkIndex; + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 7dae19c8..5b82a523 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -833,20 +833,6 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableSetupColumn("\u0394 Slow-Path Calls", ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableHeadersRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted("skin.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted("iris.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); - ImGui.TableNextColumn(); ImGui.TextUnformatted("characterglass.shpk"); ImGui.TableNextColumn(); @@ -855,18 +841,11 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterGlass}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TextUnformatted("characterlegacy.shpk"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterLegacyShpkCount}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted("charactertattoo.shpk"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterLegacy}"); ImGui.TableNextColumn(); ImGui.TextUnformatted("characterocclusion.shpk"); @@ -875,12 +854,47 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterOcclusion}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted("characterstockings.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterStockingsShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterStockings}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertattoo.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTattooShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTattoo}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("charactertransparency.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedCharacterTransparencyShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.CharacterTransparency}"); + ImGui.TableNextColumn(); ImGui.TextUnformatted("hairmask.shpk"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedHairMaskShpkCount}"); ImGui.TableNextColumn(); ImGui.TextUnformatted($"{slowPathCallDeltas.HairMask}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("iris.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedIrisShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Iris}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("skin.shpk"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_shaderReplacementFixer.ModdedSkinShpkCount}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); } /// Draw information about the resident resource files. From 03e9dc55dfebc6de1fb3290fdd6b041f547a08e2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 8 Aug 2024 23:19:44 +0200 Subject: [PATCH 342/865] Use read-only MMIO for legacy ShPk ban --- Penumbra/Interop/Processing/ShpkPathPreProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 96d9daff..2fb35ae0 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -1,3 +1,4 @@ +using System.IO.MemoryMappedFiles; using FFXIVClientStructs.FFXIV.Client.System.Resource; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -52,7 +53,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, { try { - using var file = MmioMemoryManager.CreateFromFile(path); + using var file = MmioMemoryManager.CreateFromFile(path, access: MemoryMappedFileAccess.Read); var bytes = file.GetSpan(); return ShpkFile.FastIsLegacy(bytes) From c265b917b4e56ea359a1337a06cb9cf5da155cce Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 9 Aug 2024 00:02:36 +0200 Subject: [PATCH 343/865] "This is how you end up on a list." --- Penumbra/Penumbra.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 438cdc49..d99e3fcd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,6 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", + "IllusioVitae", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From a52a43bd86cfb193204ac3bcf8d94dd8e2bf3fc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 01:15:05 +0200 Subject: [PATCH 344/865] Minor cleanup. --- .../PostProcessing/HumanSetupScalingHook.cs | 7 +-- .../PostProcessing/PreBoneDeformerReplacer.cs | 23 +++---- .../PostProcessing/ShaderReplacementFixer.cs | 61 +++++++------------ Penumbra/Interop/Services/CharacterUtility.cs | 2 +- 4 files changed, 38 insertions(+), 55 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs index 5783c099..870229d6 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/HumanSetupScalingHook.cs @@ -11,10 +11,8 @@ public unsafe class HumanSetupScalingHook : FastHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, + => Task = hooks.CreateHook("Human.SetupScaling", vTables.HumanVTable[58], Detour, !HookOverrides.Instance.PostProcessing.HumanSetupScaling); - } private void Detour(CharacterBase* drawObject, uint slotIndex) { @@ -32,6 +30,7 @@ public unsafe class HumanSetupScalingHook : FastHook replacements, ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock); - + public readonly record struct Replacement(nint AddressToReplace, nint ValueToSet, nint ValueToRestore); } diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 9273a2cb..30e643c7 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -19,7 +19,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, MetaDataComputation.All, out var p) ? p : Utf8GamePath.Empty; // Approximate name guess. - private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); + private delegate void* CharacterBaseCreateDeformerDelegate(CharacterBase* drawObject, uint slotIndex); private readonly Hook _humanCreateDeformerHook; @@ -32,12 +32,12 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public PreBoneDeformerReplacer(CharacterUtility utility, CollectionResolver collectionResolver, ResourceLoader resourceLoader, HookManager hooks, IFramework framework, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { - _utility = utility; - _collectionResolver = collectionResolver; - _resourceLoader = resourceLoader; - _framework = framework; - _humanSetupScalingHook = humanSetupScalingHook; - _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; + _utility = utility; + _collectionResolver = collectionResolver; + _resourceLoader = resourceLoader; + _framework = framework; + _humanSetupScalingHook = humanSetupScalingHook; + _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanCreateDeformerHook = hooks.CreateHook("HumanCreateDeformer", vTables.HumanVTable[101], CreateDeformer, !HookOverrides.Instance.PostProcessing.HumanCreateDeformer).Result; } @@ -45,7 +45,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi public void Dispose() { _humanCreateDeformerHook.Dispose(); - _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _humanSetupScalingHook.SetupReplacements -= SetupHssReplacements; } private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) @@ -57,18 +57,19 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); } - private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + private void SetupHssReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { if (!_framework.IsInFrameworkUpdateThread) Penumbra.Log.Warning( - $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHSSReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); + $"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread"); var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject); try { pbdDisposable = preBoneDeformer; - replacements[numReplacements++] = new((nint)(&_utility.Address->HumanPbdResource), (nint)preBoneDeformer.ResourceHandle, + replacements[numReplacements++] = new HumanSetupScalingHook.Replacement((nint)(&_utility.Address->HumanPbdResource), + (nint)preBoneDeformer.ResourceHandle, _utility.DefaultHumanPbdResource); } catch diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 20db7e25..80892b0f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -8,7 +8,6 @@ using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Hooks.Resources; -using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.Services; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; @@ -137,7 +136,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _hairMaskState = new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); - _humanSetupScalingHook.SetupReplacements += SetupHSSReplacements; + _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], OnRenderHumanMaterial, !HookOverrides.Instance.PostProcessing.HumanOnRenderMaterial).Result; _modelRendererOnRenderMaterialHook = hooks.CreateHook("ModelRenderer.OnRenderMaterial", @@ -146,7 +145,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRendererUnkFuncHook = hooks.CreateHook("ModelRenderer.UnkFunc", Sigs.ModelRendererUnkFunc, ModelRendererUnkFuncDetour, !HookOverrides.Instance.PostProcessing.ModelRendererUnkFunc).Result; - _prepareColorTableHook = hooks.CreateHook("MaterialResourceHandle.PrepareColorTable", + _prepareColorTableHook = hooks.CreateHook( + "MaterialResourceHandle.PrepareColorTable", Sigs.PrepareColorSet, PrepareColorTableDetour, !HookOverrides.Instance.PostProcessing.PrepareColorTable).Result; @@ -160,7 +160,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _modelRendererUnkFuncHook.Dispose(); _modelRendererOnRenderMaterialHook.Dispose(); _humanOnRenderMaterialHook.Dispose(); - _humanSetupScalingHook.SetupReplacements -= SetupHSSReplacements; + _humanSetupScalingHook.SetupReplacements -= SetupHssReplacements; _communicator.MtrlLoaded.Unsubscribe(OnMtrlLoaded); _resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor); @@ -188,14 +188,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic _characterOcclusionState.GetAndResetSlowPathCallDelta(), _hairMaskState.GetAndResetSlowPathCallDelta()); - private static bool IsMaterialWithShpk(MaterialResourceHandle* mtrlResource, ReadOnlySpan shpkName) - { - if (mtrlResource == null) - return false; - - return shpkName.SequenceEqual(mtrlResource->ShpkNameSpan); - } - private void OnMtrlLoaded(nint mtrlResourceHandle, nint gameObject) { var mtrl = (MaterialResourceHandle*)mtrlResourceHandle; @@ -203,9 +195,11 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; - var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) - ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); + var shpkName = mtrl->ShpkNameSpan; + var shpkState = GetStateForHumanSetup(shpkName) + ?? GetStateForHumanRender(shpkName) + ?? GetStateForModelRendererRender(shpkName) + ?? GetStateForModelRendererUnk(shpkName) ?? GetStateForColorTable(shpkName); if (shpkState != null && shpk != shpkState.DefaultShaderPackage) shpkState.TryAddMaterial(mtrlResourceHandle); @@ -228,12 +222,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) - { - if (CharacterStockingsShpkName.SequenceEqual(shpkName)) - return _characterStockingsState; - - return null; - } + => CharacterStockingsShpkName.SequenceEqual(shpkName) ? _characterStockingsState : null; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForHumanSetup() @@ -243,12 +232,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) - { - if (SkinShpkName.SequenceEqual(shpkName)) - return _skinState; - - return null; - } + => SkinShpkName.SequenceEqual(shpkName) ? _skinState : null; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForHumanRender() @@ -305,18 +289,13 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic + _characterStockingsState.MaterialCount; private ModdedShaderPackageState? GetStateForColorTable(ReadOnlySpan shpkName) - { - if (CharacterLegacyShpkName.SequenceEqual(shpkName)) - return _characterLegacyState; - - return null; - } + => CharacterLegacyShpkName.SequenceEqual(shpkName) ? _characterLegacyState : null; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private uint GetTotalMaterialCountForColorTable() => _characterLegacyState.MaterialCount; - private void SetupHSSReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, + private void SetupHssReplacements(CharacterBase* drawObject, uint slotIndex, Span replacements, ref int numReplacements, ref IDisposable? pbdDisposable, ref object? shpkLock) { // If we don't have any on-screen instances of modded characterstockings.shpk, we don't need the slow path at all. @@ -326,6 +305,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic var model = drawObject->Models[slotIndex]; if (model == null) return; + MaterialResourceHandle* mtrlResource = null; ModdedShaderPackageState? shpkState = null; foreach (var material in model->MaterialsSpan) @@ -340,6 +320,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpkState != null) break; } + if (shpkState == null || shpkState.MaterialCount == 0) return; @@ -348,7 +329,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic // This is less performance-critical than the others, as this is called by the game only on draw object creation and slot update. // There are still thread safety concerns as it might be called in other threads by plugins. shpkLock = shpkState; - replacements[numReplacements++] = new((nint)shpkState.ShaderPackageReference, (nint)mtrlResource->ShaderPackageResourceHandle, + replacements[numReplacements++] = new HumanSetupScalingHook.Replacement((nint)shpkState.ShaderPackageReference, + (nint)mtrlResource->ShaderPackageResourceHandle, (nint)shpkState.DefaultShaderPackage); } @@ -439,7 +421,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic // Same performance considerations as OnRenderHumanMaterial. lock (shpkState) { - var shpkReference = shpkState.ShaderPackageReference; + var shpkReference = shpkState.ShaderPackageReference; try { *shpkReference = mtrlResource->ShaderPackageResourceHandle; @@ -452,7 +434,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } } - private MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) + private static MaterialResourceHandle* GetMaterialResourceHandle(ModelRendererStructs.UnkPayload* unkPayload) { // TODO ClientStructs-ify var unkPointer = *(nint*)((nint)unkPayload->ModelResourceHandle + 0xE8) + unkPayload->UnkIndex * 0x24; @@ -467,13 +449,14 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (mtrlResource->ShaderPackageResourceHandle == null) { - Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); + Penumbra.Log.Warning("ShaderReplacementFixer found a MaterialResourceHandle with no shader package"); return null; } if (mtrlResource->ShaderPackageResourceHandle->ShaderPackage != unkPayload->ShaderWrapper->ShaderPackage) { - Penumbra.Log.Warning($"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); + Penumbra.Log.Warning( + $"ShaderReplacementFixer found a MaterialResourceHandle (0x{(nint)mtrlResource:X}) with an inconsistent shader package (got 0x{(nint)mtrlResource->ShaderPackageResourceHandle->ShaderPackage:X}, expected 0x{(nint)unkPayload->ShaderWrapper->ShaderPackage:X})"); return null; } diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 4ab156a9..1641e42d 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -67,7 +67,7 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService _framework.Update += LoadDefaultResources; } - /// We store the default data of the resources so we can always restore them. + /// We store the default data of the resources, so we can always restore them. private void LoadDefaultResources(object _) { if (Address == null) From 465e65e8fec7a3a3c1a51174ee99955390c15506 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 8 Aug 2024 23:17:52 +0000 Subject: [PATCH 345/865] [CI] Updating repo.json for testing_1.2.0.23 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3de0c5c8..efb71542 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.1.1.2", - "TestingAssemblyVersion": "1.2.0.22", + "TestingAssemblyVersion": "1.2.0.23", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.22/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.23/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From e44b450548f42bac821d3c3746a868e976aec420 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 22:17:23 +0200 Subject: [PATCH 346/865] Disable model import/export for now. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index de088736..490fa147 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -97,7 +97,9 @@ public partial class ModEditWindow private void DrawImportExport(MdlTab tab, bool disabled) { - if (!ImGui.CollapsingHeader("Import / Export")) + // TODO: Enable when functional. + using var dawntrailDisabled = ImRaii.Disabled(); + if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true) return; var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); From b5f7f03e11cc9dc26555667f3f90448413d20d3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 22:46:34 +0200 Subject: [PATCH 347/865] Update BNPC Names. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2fd5aa44..bf020ebf 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2fd5aa44056a906df90c9a826d1d17f6fdafebff +Subproject commit bf020ebf5e4980f1814b336aabbaba5e2e00c362 From 1b671b95ab2c95b4af554430746f18d82d50e806 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 23:04:07 +0200 Subject: [PATCH 348/865] 1.2.1.0 --- Penumbra/UI/Changelog.cs | 63 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index f4cedf7d..55ce70e4 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -52,18 +52,68 @@ public class PenumbraChangelog : IUiService AddDummy(Changelog); Add1_1_0_0(Changelog); Add1_1_1_0(Changelog); - } - + Add1_2_1_0(Changelog); + } + #region Changelogs + private static void Add1_2_1_0(Changelog log) + => log.NextVersion("Version 1.2.1.0") + .RegisterHighlight("Penumbra is now released for Dawntrail.") + .RegisterEntry("Mods themselves may have to be updated. TexTools provides options for this.", 1) + .RegisterEntry("For model files, Penumbra provides a rudimentary update function, but prefer using TexTools if possible.", 1) + .RegisterEntry("Other files, like materials and textures, will have to go through TexTools for the moment.", 1) + .RegisterImportant("I am sorry that it took this long, but there was an immense amount of work to be done from the start.") + .RegisterImportant( + "Since Penumbra has been in Testing for quite a while, multitudes of bugs and issues cropped up that needed to be dealt with.", + 1) + .RegisterEntry("There very well may still be a lot of issues, so please report any you find.", 1) + .RegisterImportant("BUT, please make sure that those issues are not caused by outdated mods before reporting them.", 1) + .RegisterEntry( + "This changelog may seem rather short for the timespan, but I omitted hundreds of smaller fixes and the details of getting Penumbra to work in Dawntrail.", + 1) + .RegisterHighlight("The Material Editing tab in the Advanced Editing Window has been heavily improved (by Ny).") + .RegisterEntry( + "Especially for Dawntrail materials using the new shaders, the window provides much more in-depth and user-friendly editing options.", + 1) + .RegisterHighlight("Many advancements regarding modded shaders, and modding bone deformers have been made.") + .RegisterHighlight("IMC groups now allow their options to toggle attributes off that are on in the default entry.") + .RegisterImportant( + "The 'Update Bibo' button was removed. The functionality is redundant since any mods that old need to be updated anyway.") + .RegisterEntry("Clicking the button on modern mods generally caused more harm than benefit.", 1) + .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") + .RegisterEntry( + "If you somehow still need to mass-migrate materials in your models, the Material Reassignment tab in Advanced Editing is still available for this.", + 1) + .RegisterHighlight("You can now change a mods state in any collection from its Collections tab via right-clicking the state.") + .RegisterHighlight("Items changed in a mod now sort before other items in the Item Swap tab, and are highlighted.") + .RegisterEntry("Path handling was improved in regards to case-sensitivity.") + .RegisterEntry("Fixed an issue with negative search matching on folders with no matches") + .RegisterEntry("Mod option groups on the same priority are now applied in reverse index order. (1.2.0.12)") + .RegisterEntry("Fixed the display of missing files in the Advanced Editing Window's header. (1.2.0.8)") + .RegisterEntry( + "Fixed some, but not all soft-locks that occur when your character gets redrawn while fishing. Just do not do that. (1.2.0.7)") + .RegisterEntry("Improved handling of invalid Offhand IMC files for certain jobs. (1.2.0.6)") + .RegisterEntry("Added automatic reduplication for files in the UI category, as they cause crashes when not unique. (1.2.0.5)") + .RegisterEntry("The mod import popup can now be closed by clicking outside of it, if it is finished. (1.2.0.5)") + .RegisterEntry("Fixed an issue with Mod Normalization skipping the default option. (1.2.0.5)") + .RegisterEntry("Improved the Support Info output. (1.1.1.5)") + .RegisterEntry("Reworked the handling of Meta Manipulations entirely. (1.1.1.3)") + .RegisterEntry("Added a configuration option to disable showing mods in the character lobby and at the aesthetician. (1.1.1.1)") + .RegisterEntry("Fixed an issue with the AddMods API and the root directory. (1.1.1.2)") + .RegisterEntry("Fixed an issue with the Mod Merger file lookup and casing. (1.1.1.2)") + .RegisterEntry("Fixed an issue with file saving not happening when merging mods or swapping items in some cases. (1.1.1.2)"); + private static void Add1_1_1_0(Changelog log) => log.NextVersion("Version 1.1.1.0") .RegisterHighlight("Filtering for mods is now tokenized and can filter for multiple things at once, or exclude specific things.") .RegisterEntry("Hover over the filter to see the new available options in the tooltip.", 1) - .RegisterEntry("Be aware that the tokenization changed the prior behavior slightly.", 1) - .RegisterEntry("This is open to improvements, if you have any ideas, let me know!", 1) + .RegisterEntry("Be aware that the tokenization changed the prior behavior slightly.", 1) + .RegisterEntry("This is open to improvements, if you have any ideas, let me know!", 1) .RegisterHighlight("Added initial identification of characters in the login-screen by name.") - .RegisterEntry("Those characters can not be redrawn and re-use some things, so this may not always behave as expected, but should work in general. Let me know if you encounter edge cases!", 1) + .RegisterEntry( + "Those characters can not be redrawn and re-use some things, so this may not always behave as expected, but should work in general. Let me know if you encounter edge cases!", + 1) .RegisterEntry("Added functionality for IMC groups to apply to all variants for a model instead of a specific one.") .RegisterEntry("Improved the resource tree view with filters and incognito mode. (by Ny)") .RegisterEntry("Added a tooltip to the global EQP condition.") @@ -131,7 +181,8 @@ public class PenumbraChangelog : IUiService .RegisterEntry( "Made some improvements to the Advanced Editing window, for example a much better and more performant Hex Viewer for unstructured data was added.") .RegisterEntry("Various improvements to model import/export by ackwell (throughout all patches).") - .RegisterEntry("Hovering over meta manipulations in other options in the advanced editing window now shows a list of those options.") + .RegisterEntry( + "Hovering over meta manipulations in other options in the advanced editing window now shows a list of those options.") .RegisterEntry("Reworked the API and IPC structure heavily.") .RegisterImportant("This means some plugins interacting with Penumbra may not work correctly until they update.", 1) .RegisterEntry("Worked around the UI IPC possibly displacing all settings when the drawn additions became too big.") From 741141f22769fe8af8991545867d44082a65a466 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 23:52:12 +0200 Subject: [PATCH 349/865] Woops. --- Penumbra/UI/Changelog.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 55ce70e4..5bf4f59d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -59,7 +59,7 @@ public class PenumbraChangelog : IUiService private static void Add1_2_1_0(Changelog log) => log.NextVersion("Version 1.2.1.0") - .RegisterHighlight("Penumbra is now released for Dawntrail.") + .RegisterHighlight("Penumbra is now released for Dawntrail!") .RegisterEntry("Mods themselves may have to be updated. TexTools provides options for this.", 1) .RegisterEntry("For model files, Penumbra provides a rudimentary update function, but prefer using TexTools if possible.", 1) .RegisterEntry("Other files, like materials and textures, will have to go through TexTools for the moment.", 1) @@ -81,10 +81,10 @@ public class PenumbraChangelog : IUiService .RegisterImportant( "The 'Update Bibo' button was removed. The functionality is redundant since any mods that old need to be updated anyway.") .RegisterEntry("Clicking the button on modern mods generally caused more harm than benefit.", 1) - .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") .RegisterEntry( "If you somehow still need to mass-migrate materials in your models, the Material Reassignment tab in Advanced Editing is still available for this.", 1) + .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") .RegisterHighlight("You can now change a mods state in any collection from its Collections tab via right-clicking the state.") .RegisterHighlight("Items changed in a mod now sort before other items in the Item Swap tab, and are highlighted.") .RegisterEntry("Path handling was improved in regards to case-sensitivity.") From 421fde70b08db075c359bd699a079bf64e8297e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 9 Aug 2024 23:57:42 +0200 Subject: [PATCH 350/865] Addendum --- Penumbra/UI/Changelog.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 5bf4f59d..41920d1c 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -63,6 +63,8 @@ public class PenumbraChangelog : IUiService .RegisterEntry("Mods themselves may have to be updated. TexTools provides options for this.", 1) .RegisterEntry("For model files, Penumbra provides a rudimentary update function, but prefer using TexTools if possible.", 1) .RegisterEntry("Other files, like materials and textures, will have to go through TexTools for the moment.", 1) + .RegisterEntry( + "Some outdated mods can be identified by Penumbra and are prevented from loading entirely (specifically shaders, by Ny).", 1) .RegisterImportant("I am sorry that it took this long, but there was an immense amount of work to be done from the start.") .RegisterImportant( "Since Penumbra has been in Testing for quite a while, multitudes of bugs and issues cropped up that needed to be dealt with.", @@ -84,6 +86,7 @@ public class PenumbraChangelog : IUiService .RegisterEntry( "If you somehow still need to mass-migrate materials in your models, the Material Reassignment tab in Advanced Editing is still available for this.", 1) + .RegisterEntry("The On-Screen tab was updated and improved and can now display modded actual paths in more useful form.") .RegisterImportant("Model Import/Export is temporarily disabled until Dawntrail-related changes can be made.") .RegisterHighlight("You can now change a mods state in any collection from its Collections tab via right-clicking the state.") .RegisterHighlight("Items changed in a mod now sort before other items in the Item Swap tab, and are highlighted.") From 7ba7a6e31915ba84c05fd41ffc46b486b67e9caa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Aug 2024 11:55:30 +0200 Subject: [PATCH 351/865] API 5.3 --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 759a8e9d..552246e5 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 759a8e9dc50b3453cdb7c3cba76de7174c94aba0 +Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 0400c694..eaaf9f38 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 1); + => (5, 3); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; From ccce087b8706b95997f2c97e8df7c8ee3d9ff44c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Aug 2024 11:59:18 +0200 Subject: [PATCH 352/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index bf020ebf..3a65ed1c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bf020ebf5e4980f1814b336aabbaba5e2e00c362 +Subproject commit 3a65ed1c86a2d5fd5794ff5c0559b02fc25d7224 From 5c9e158da36bbae9fb1299da16ef918b1aef1417 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 10 Aug 2024 10:01:17 +0000 Subject: [PATCH 353/865] [CI] Updating repo.json for 1.2.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index efb71542..0e5c7799 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.1.1.2", - "TestingAssemblyVersion": "1.2.0.23", + "AssemblyVersion": "1.2.1.0", + "TestingAssemblyVersion": "1.2.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 9, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.0.23/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.1.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a27ce6b0a78243708c1134fcc945a09f05796351 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 10 Aug 2024 12:23:17 +0200 Subject: [PATCH 354/865] Update non-testing DalamudApiLevel. --- Penumbra.sln | 3 ++- repo.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 78fa1543..46609f85 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -8,6 +8,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + repo.json = repo.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" @@ -18,7 +19,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.Api", "Penumbra.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra.String\Penumbra.String.csproj", "{5549BAFD-6357-4B1A-800C-75AC36E5B76D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/repo.json b/repo.json index 0e5c7799..73ae9eff 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.2.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 9, + "DalamudApiLevel": 10, "TestingDalamudApiLevel": 10, "IsHide": "False", "IsTestingExclusive": "False", From 7710d9249675e6550f9db2eaaf94e1c570929c23 Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Sat, 10 Aug 2024 18:59:02 +0200 Subject: [PATCH 355/865] Add another plugin to Copy Support Info --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index d99e3fcd..6f0b63ce 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", + "IllusioVitae", "Aetherment", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From 6b0b1629bd2e828d6f237fe64f84c84d6066534c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 11 Aug 2024 23:22:05 +0200 Subject: [PATCH 356/865] Move GuidExtensions to OtterGui (unused atm) --- OtterGui | 2 +- Penumbra/GuidExtensions.cs | 254 ------------------------------------- 2 files changed, 1 insertion(+), 255 deletions(-) delete mode 100644 Penumbra/GuidExtensions.cs diff --git a/OtterGui b/OtterGui index d9486ae5..07a00913 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d9486ae54b5a4b61cf74f79ed27daa659eb1ce5b +Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a diff --git a/Penumbra/GuidExtensions.cs b/Penumbra/GuidExtensions.cs deleted file mode 100644 index fcbc8a3b..00000000 --- a/Penumbra/GuidExtensions.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System.Collections.Frozen; -using OtterGui; - -namespace Penumbra; - -public static class GuidExtensions -{ - private const string Chars = - "0123456789" - + "abcdefghij" - + "klmnopqrst" - + "uv"; - - private static ReadOnlySpan Bytes - => "0123456789abcdefghijklmnopqrstuv"u8; - - private static readonly FrozenDictionary - ReverseChars = Chars.WithIndex().ToFrozenDictionary(t => t.Value, t => (byte)t.Index); - - private static readonly FrozenDictionary ReverseBytes = - ReverseChars.ToFrozenDictionary(kvp => (byte)kvp.Key, kvp => kvp.Value); - - public static unsafe string OptimizedString(this Guid guid) - { - var bytes = stackalloc ulong[2]; - if (!guid.TryWriteBytes(new Span(bytes, 16))) - return guid.ToString("N"); - - var u1 = bytes[0]; - var u2 = bytes[1]; - Span text = - [ - Chars[(int)(u1 & 0x1F)], - Chars[(int)((u1 >> 5) & 0x1F)], - Chars[(int)((u1 >> 10) & 0x1F)], - Chars[(int)((u1 >> 15) & 0x1F)], - Chars[(int)((u1 >> 20) & 0x1F)], - Chars[(int)((u1 >> 25) & 0x1F)], - Chars[(int)((u1 >> 30) & 0x1F)], - Chars[(int)((u1 >> 35) & 0x1F)], - Chars[(int)((u1 >> 40) & 0x1F)], - Chars[(int)((u1 >> 45) & 0x1F)], - Chars[(int)((u1 >> 50) & 0x1F)], - Chars[(int)((u1 >> 55) & 0x1F)], - Chars[(int)((u1 >> 60) | ((u2 & 0x01) << 4))], - Chars[(int)((u2 >> 1) & 0x1F)], - Chars[(int)((u2 >> 6) & 0x1F)], - Chars[(int)((u2 >> 11) & 0x1F)], - Chars[(int)((u2 >> 16) & 0x1F)], - Chars[(int)((u2 >> 21) & 0x1F)], - Chars[(int)((u2 >> 26) & 0x1F)], - Chars[(int)((u2 >> 31) & 0x1F)], - Chars[(int)((u2 >> 36) & 0x1F)], - Chars[(int)((u2 >> 41) & 0x1F)], - Chars[(int)((u2 >> 46) & 0x1F)], - Chars[(int)((u2 >> 51) & 0x1F)], - Chars[(int)((u2 >> 56) & 0x1F)], - Chars[(int)((u2 >> 61) & 0x1F)], - ]; - return new string(text); - } - - public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) - { - if (text.Length != 26) - return Return(out guid); - - var bytes = stackalloc ulong[2]; - if (!ReverseChars.TryGetValue(text[0], out var b0)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[1], out var b1)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[2], out var b2)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[3], out var b3)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[4], out var b4)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[5], out var b5)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[6], out var b6)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[7], out var b7)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[8], out var b8)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[9], out var b9)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[10], out var b10)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[11], out var b11)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[12], out var b12)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[13], out var b13)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[14], out var b14)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[15], out var b15)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[16], out var b16)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[17], out var b17)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[18], out var b18)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[19], out var b19)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[20], out var b20)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[21], out var b21)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[22], out var b22)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[23], out var b23)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[24], out var b24)) - return Return(out guid); - if (!ReverseChars.TryGetValue(text[25], out var b25)) - return Return(out guid); - - bytes[0] = b0 - | ((ulong)b1 << 5) - | ((ulong)b2 << 10) - | ((ulong)b3 << 15) - | ((ulong)b4 << 20) - | ((ulong)b5 << 25) - | ((ulong)b6 << 30) - | ((ulong)b7 << 35) - | ((ulong)b8 << 40) - | ((ulong)b9 << 45) - | ((ulong)b10 << 50) - | ((ulong)b11 << 55) - | ((ulong)b12 << 60); - bytes[1] = ((ulong)b12 >> 4) - | ((ulong)b13 << 1) - | ((ulong)b14 << 6) - | ((ulong)b15 << 11) - | ((ulong)b16 << 16) - | ((ulong)b17 << 21) - | ((ulong)b18 << 26) - | ((ulong)b19 << 31) - | ((ulong)b20 << 36) - | ((ulong)b21 << 41) - | ((ulong)b22 << 46) - | ((ulong)b23 << 51) - | ((ulong)b24 << 56) - | ((ulong)b25 << 61); - guid = new Guid(new Span(bytes, 16)); - return true; - - static bool Return(out Guid guid) - { - guid = Guid.Empty; - return false; - } - } - - public static unsafe bool FromOptimizedString(ReadOnlySpan text, out Guid guid) - { - if (text.Length != 26) - return Return(out guid); - - var bytes = stackalloc ulong[2]; - if (!ReverseBytes.TryGetValue(text[0], out var b0)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[1], out var b1)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[2], out var b2)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[3], out var b3)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[4], out var b4)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[5], out var b5)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[6], out var b6)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[7], out var b7)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[8], out var b8)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[9], out var b9)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[10], out var b10)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[11], out var b11)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[12], out var b12)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[13], out var b13)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[14], out var b14)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[15], out var b15)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[16], out var b16)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[17], out var b17)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[18], out var b18)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[19], out var b19)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[20], out var b20)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[21], out var b21)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[22], out var b22)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[23], out var b23)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[24], out var b24)) - return Return(out guid); - if (!ReverseBytes.TryGetValue(text[25], out var b25)) - return Return(out guid); - - bytes[0] = b0 - | ((ulong)b1 << 5) - | ((ulong)b2 << 10) - | ((ulong)b3 << 15) - | ((ulong)b4 << 20) - | ((ulong)b5 << 25) - | ((ulong)b6 << 30) - | ((ulong)b7 << 35) - | ((ulong)b8 << 40) - | ((ulong)b9 << 45) - | ((ulong)b10 << 50) - | ((ulong)b11 << 55) - | ((ulong)b12 << 60); - bytes[1] = ((ulong)b12 >> 4) - | ((ulong)b13 << 1) - | ((ulong)b14 << 6) - | ((ulong)b15 << 11) - | ((ulong)b16 << 16) - | ((ulong)b17 << 21) - | ((ulong)b18 << 26) - | ((ulong)b19 << 31) - | ((ulong)b20 << 36) - | ((ulong)b21 << 41) - | ((ulong)b22 << 46) - | ((ulong)b23 << 51) - | ((ulong)b24 << 56) - | ((ulong)b25 << 61); - guid = new Guid(new Span(bytes, 16)); - return true; - - static bool Return(out Guid guid) - { - guid = Guid.Empty; - return false; - } - } -} From 47268ab377db99a91cfc14e7560e459bbb095171 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 11 Aug 2024 23:22:42 +0200 Subject: [PATCH 357/865] Prevent loading crashy shpks. --- Penumbra/Interop/PathResolving/PathResolver.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 67ec4fc3..63bbc8d8 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -61,7 +61,7 @@ public class PathResolver : IDisposable, IService ResourceCategory.GameScript => (null, ResolveData.Invalid), // Use actual resolving. ResourceCategory.Chara => Resolve(path, resourceType), - ResourceCategory.Shader => Resolve(path, resourceType), + ResourceCategory.Shader => ResolveShader(path, resourceType), ResourceCategory.Vfx => Resolve(path, resourceType), ResourceCategory.Sound => Resolve(path, resourceType), // EXD Modding in general should probably be prohibited but is currently used for fan translations. @@ -83,6 +83,19 @@ public class PathResolver : IDisposable, IService }; } + /// Replacing the characterstockings.shpk or the characterocclusion.shpk files currently causes crashes, so we just entirely prevent that. + private (FullPath?, ResolveData) ResolveShader(Utf8GamePath gamePath, ResourceType type) + { + if (type is not ResourceType.Shpk) + return Resolve(gamePath, type); + + if (gamePath.Path.EndsWith("occlusion.shpk"u8) + || gamePath.Path.EndsWith("stockings.shpk"u8)) + return (null, ResolveData.Invalid); + + return Resolve(gamePath, type); + } + public (FullPath?, ResolveData) Resolve(Utf8GamePath gamePath, ResourceType type) { using var performance = _performance.Measure(PerformanceType.CharacterResolver); From 8ee326853d3e15c8b4e949dfdacfb0e5f5c1c817 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 11 Aug 2024 23:24:18 +0200 Subject: [PATCH 358/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 3a65ed1c..b7fdfe9d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3a65ed1c86a2d5fd5794ff5c0559b02fc25d7224 +Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee From 6e351aa68b5903ff0b691eed43ed1c1d72ae65ff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:57:06 +0200 Subject: [PATCH 359/865] Make mods added via API migrate models if enabled. --- Penumbra/Api/Api/ModsApi.cs | 11 ++++++++++- Penumbra/Services/MigrationManager.cs | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 790121d5..2acdf031 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -15,15 +15,17 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable private readonly ModImportManager _modImportManager; private readonly Configuration _config; private readonly ModFileSystem _modFileSystem; + private readonly MigrationManager _migrationManager; public ModsApi(ModManager modManager, ModImportManager modImportManager, Configuration config, ModFileSystem modFileSystem, - CommunicatorService communicator) + CommunicatorService communicator, MigrationManager migrationManager) { _modManager = modManager; _modImportManager = modImportManager; _config = config; _modFileSystem = modFileSystem; _communicator = communicator; + _migrationManager = migrationManager; _communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ApiMods); } @@ -81,9 +83,16 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); _modManager.AddMod(dir); + if (_config.MigrateImportedModelsToV6) + { + _migrationManager.MigrateMdlDirectory(dir.FullName, false); + _migrationManager.Await(); + } + if (_config.UseFileSystemCompression) new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); + return ApiHelpers.Return(PenumbraApiEc.Success, args); } diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 7726f6fd..9041fbd0 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -63,6 +63,9 @@ public class MigrationManager(Configuration config) : IService public void CleanMtrlBackups(string path) => CleanBackups(path, "*.mtrl.bak", "material", MtrlCleanup, TaskType.MtrlCleanup); + public void Await() + => _currentTask?.Wait(); + private void CleanBackups(string path, string extension, string fileType, MigrationData data, TaskType type) { if (IsRunning) From 3135d5e7e656cc37b6a60744f3cb63d50ca79b08 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:57:38 +0200 Subject: [PATCH 360/865] Make collection combo mousewheel-scrollable with ctrl. --- Penumbra/UI/CollectionTab/CollectionCombo.cs | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 9d195eed..1670be5e 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,14 +1,15 @@ using ImGuiNET; +using OtterGui; using OtterGui.Raii; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; -using Penumbra.GameData.Actors; namespace Penumbra.UI.CollectionTab; public sealed class CollectionCombo(CollectionManager manager, Func> items) - : FilterComboCache(items, MouseWheelType.None, Penumbra.Log) + : FilterComboCache(items, MouseWheelType.Control, Penumbra.Log) { private readonly ImRaii.Color _color = new(); @@ -20,14 +21,26 @@ public sealed class CollectionCombo(CollectionManager manager, Func obj.Name; + + protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, + ImGuiComboFlags flags) + { + base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); + ImUtf8.HoverTooltip("Control and mouse wheel to scroll."u8); + } } From bb9dd184a369513bd872cdb2e5c866f0d1b96b33 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:57:52 +0200 Subject: [PATCH 361/865] Fix order of gender and model in EQDP drawer. --- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 970b70cb..5206ece8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -86,11 +86,11 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil ImUtf8.HoverTooltip("Model Set ID"u8); ImGui.TableNextColumn(); - ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); ImUtf8.HoverTooltip("Model Race"u8); ImGui.TableNextColumn(); - ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.TextFramed(identifier.Gender.ToName(), FrameColor); ImUtf8.HoverTooltip("Gender"u8); ImGui.TableNextColumn(); From bedf5dab794b1ddbe4a2c5edcd60074d239963f4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 12 Aug 2024 20:58:17 +0200 Subject: [PATCH 362/865] Make collection resolver not cache early resolved actors that aren't characters. --- Penumbra/Interop/PathResolving/CollectionResolver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 136da0f5..313c4f8b 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -216,10 +216,13 @@ public sealed unsafe class CollectionResolver( private ModCollection? CollectionByAttributes(Actor actor, ref bool notYetReady) { if (!actor.IsCharacter) + { + Penumbra.Log.Excessive($"Actor to be identified was not yet a Character."); + notYetReady = true; return null; + } // Only handle human models. - if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) return null; From 1da095be9946e9fa2879ef8b61d201f25b3086d6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 12 Aug 2024 19:00:43 +0000 Subject: [PATCH 363/865] [CI] Updating repo.json for 1.2.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 73ae9eff..5a274d73 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.2.1.0", - "TestingAssemblyVersion": "1.2.1.0", + "AssemblyVersion": "1.2.1.1", + "TestingAssemblyVersion": "1.2.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 96f0479b53b62b41f9ccd136c87884d35ecc1234 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:42:29 +0200 Subject: [PATCH 364/865] Some cleanup. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs | 1 - Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 1 - Penumbra/UI/Tabs/Debug/DebugTab.cs | 5 ++--- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/OtterGui b/OtterGui index 07a00913..276327f8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 07a009134bf5eb7da9a54ba40e82c88fc613544a +Subproject commit 276327f812e2f7e6aac7aee9e5ef0a560b065765 diff --git a/Penumbra.GameData b/Penumbra.GameData index b7fdfe9d..c8708ec5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b7fdfe9d19f7e3229834480db446478b0bf6acee +Subproject commit c8708ec5153cb60c9e43b2c53d02b81b2c8175f9 diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 4aae45a2..515f6ff4 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -2,7 +2,6 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.GameData.Structs; -using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 9d1ab78a..4ab1c6aa 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -4,7 +4,6 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget; -using OtterGui.Widgets; using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5b82a523..7c6cd01e 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1,5 +1,4 @@ using Dalamud.Interface; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; @@ -43,7 +42,6 @@ using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; -using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -721,7 +719,8 @@ public class DebugTab : Window, ITab, IUiService if (!tree) continue; - using var table = Table("##table", data.Colors.Length + data.Scalars.Length, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + using var table = Table("##table", data.Colors.Length + data.Scalars.Length, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; From 35492837690b93761a7e9c2c52f2c20cb28b5765 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:42:47 +0200 Subject: [PATCH 365/865] Order meta entries. --- .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 5 ++++- .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 5 ++++- .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 6 +++++- .../Meta/GlobalEqpMetaDrawer.cs | 5 ++++- .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 4 +++- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 21 ++++++++++++------- .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 5 ++++- 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index 5206ece8..f586045c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -61,7 +61,10 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil } protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() - => Editor.Eqdp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId) + .ThenBy(kvp => kvp.Key.GenderRace) + .ThenBy(kvp => kvp.Key.Slot) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 56c06bc9..b1031b44 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -59,7 +59,10 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() - => Editor.Eqp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Eqp + .OrderBy(kvp => kvp.Key.SetId) + .ThenBy(kvp => kvp.Key.Slot) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref EqpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 5c3c5df5..628cee40 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -58,7 +58,11 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() - => Editor.Est.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Est + .OrderBy(kvp => kvp.Key.SetId) + .ThenBy(kvp => kvp.Key.GenderRace) + .ThenBy(kvp => kvp.Key.Slot) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref EstIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 130831a0..bc2e1bde 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -47,7 +47,10 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me } protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() - => Editor.GlobalEqp.Select(identifier => (identifier, (byte)0)); + => Editor.GlobalEqp + .OrderBy(identifier => identifier.Type) + .ThenBy(identifier => identifier.Condition) + .Select(identifier => (identifier, (byte)0)); private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 87ed21dc..1e91731d 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -57,7 +57,9 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() - => Editor.Gmp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Gmp + .OrderBy(kvp => kvp.Key.SetId) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref GmpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 58f626fc..e33eb1aa 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -140,7 +140,14 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() - => Editor.Imc.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Imc + .OrderBy(kvp => kvp.Key.ObjectType) + .ThenBy(kvp => kvp.Key.PrimaryId) + .ThenBy(kvp => kvp.Key.EquipSlot) + .ThenBy(kvp => kvp.Key.BodySlot) + .ThenBy(kvp => kvp.Key.SecondaryId) + .ThenBy(kvp => kvp.Key.Variant) + .Select(kvp => (kvp.Key, kvp.Value)); public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { @@ -149,18 +156,18 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile if (ret) { - var equipSlot = type switch + var (equipSlot, secondaryId) = type switch { - ObjectType.Equipment => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.DemiHuman => identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, - ObjectType.Accessory => identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, - _ => EquipSlot.Unknown, + ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0), + ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), + ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0), + _ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), }; identifier = identifier with { ObjectType = type, EquipSlot = equipSlot, - SecondaryId = identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId, + SecondaryId = secondaryId, }; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index be02e321..6d819b16 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -58,7 +58,10 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } protected override IEnumerable<(RspIdentifier, RspEntry)> Enumerate() - => Editor.Rsp.Select(kvp => (kvp.Key, kvp.Value)); + => Editor.Rsp + .OrderBy(kvp => kvp.Key.SubRace) + .ThenBy(kvp => kvp.Key.Attribute) + .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref RspIdentifier identifier) { From a2237773e315be9f3209ab8c6a462e5cdcf8837e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:43:11 +0200 Subject: [PATCH 366/865] Update packages. --- Penumbra/Import/Models/Export/MeshExporter.cs | 13 +- .../Import/Models/Export/VertexFragment.cs | 140 ++++++++++++------ .../Import/Models/Import/SubMeshImporter.cs | 2 +- Penumbra/Import/TexToolsImport.cs | 2 +- Penumbra/Import/TexToolsImporter.Archives.cs | 12 +- Penumbra/Penumbra.csproj | 8 +- Penumbra/Services/MigrationManager.cs | 2 +- Penumbra/packages.lock.json | 54 ++++--- 8 files changed, 139 insertions(+), 94 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 219a046e..3a57ab55 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -1,4 +1,6 @@ using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Nodes; using Lumina.Extensions; using OtterGui; using Penumbra.GameData.Files; @@ -23,11 +25,11 @@ public class MeshExporter ? scene.AddSkinnedMesh(data.Mesh, Matrix4x4.Identity, [.. skeleton.Value.Joints]) : scene.AddRigidMesh(data.Mesh, Matrix4x4.Identity); - var extras = new Dictionary(data.Attributes.Length); + var node = new JsonObject(); foreach (var attribute in data.Attributes) - extras.Add(attribute, true); + node[attribute] = true; - instance.WithExtras(JsonContent.CreateFrom(extras)); + instance.WithExtras(node); } } } @@ -233,10 +235,7 @@ public class MeshExporter // Named morph targets aren't part of the specification, however `MESH.extras.targetNames` // is a commonly-accepted means of providing the data. - meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary() - { - { "targetNames", shapeNames }, - }); + meshBuilder.Extras = new JsonObject { ["targetNames"] = JsonSerializer.SerializeToNode(shapeNames) }; string[] attributes = []; var maxAttribute = 31 - BitOperations.LeadingZeroCount(attributeMask); diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index 7a82e994..eff34d54 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -1,4 +1,6 @@ +using System; using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Memory; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Export; @@ -11,35 +13,40 @@ and there's reason to overhaul the export pipeline. public struct VertexColorFfxiv : IVertexCustom { - // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] + public IEnumerable> GetEncodingAttributes() + { + // NOTE: We only realistically require UNSIGNED_BYTE for this, however Blender 3.6 errors on that (fixed in 4.0). + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + public Vector4 FfxivColor; - public int MaxColors => 0; + public int MaxColors + => 0; - public int MaxTextCoords => 0; + public int MaxTextCoords + => 0; private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; - public IEnumerable CustomAttributes => CustomNames; + + public IEnumerable CustomAttributes + => CustomNames; public VertexColorFfxiv(Vector4 ffxivColor) - { - FfxivColor = ffxivColor; - } + => FfxivColor = ffxivColor; public void Add(in VertexMaterialDelta delta) - { - } + { } public VertexMaterialDelta Subtract(IVertexMaterial baseValue) - => new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero); + => 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) { @@ -65,12 +72,17 @@ public struct VertexColorFfxiv : IVertexCustom => throw new ArgumentOutOfRangeException(nameof(index)); public void SetColor(int setIndex, Vector4 color) - { - } + { } public void Validate() { - var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; if (components.Any(component => component < 0 || component > 1)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } @@ -78,22 +90,32 @@ public struct VertexColorFfxiv : IVertexCustom public struct VertexTexture1ColorFfxiv : IVertexCustom { - [VertexAttribute("TEXCOORD_0")] + public IEnumerable> GetEncodingAttributes() + { + yield return new KeyValuePair("TEXCOORD_0", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + public Vector2 TexCoord0; - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; - public int MaxColors => 0; + public int MaxColors + => 0; - public int MaxTextCoords => 1; + public int MaxTextCoords + => 1; private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; - public IEnumerable CustomAttributes => CustomNames; + + public IEnumerable CustomAttributes + => CustomNames; public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) { - TexCoord0 = texCoord0; + TexCoord0 = texCoord0; FfxivColor = ffxivColor; } @@ -103,9 +125,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom } public VertexMaterialDelta Subtract(IVertexMaterial baseValue) - { - return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); - } + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero); public Vector2 GetTexCoord(int index) => index switch @@ -116,8 +136,10 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public void SetTexCoord(int setIndex, Vector2 coord) { - if (setIndex == 0) TexCoord0 = coord; - if (setIndex >= 1) throw new ArgumentOutOfRangeException(nameof(setIndex)); + if (setIndex == 0) + TexCoord0 = coord; + if (setIndex >= 1) + throw new ArgumentOutOfRangeException(nameof(setIndex)); } public bool TryGetCustomAttribute(string attributeName, out object? value) @@ -144,12 +166,17 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom => throw new ArgumentOutOfRangeException(nameof(index)); public void SetColor(int setIndex, Vector4 color) - { - } + { } public void Validate() { - var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; if (components.Any(component => component < 0 || component > 1)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } @@ -157,26 +184,35 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public struct VertexTexture2ColorFfxiv : IVertexCustom { - [VertexAttribute("TEXCOORD_0")] + 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", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + public Vector2 TexCoord0; - - [VertexAttribute("TEXCOORD_1")] public Vector2 TexCoord1; - - [VertexAttribute("_FFXIV_COLOR", EncodingType.UNSIGNED_SHORT, true)] public Vector4 FfxivColor; - public int MaxColors => 0; + public int MaxColors + => 0; - public int MaxTextCoords => 2; + public int MaxTextCoords + => 2; private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; - public IEnumerable CustomAttributes => CustomNames; + + public IEnumerable CustomAttributes + => CustomNames; public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; + TexCoord0 = texCoord0; + TexCoord1 = texCoord1; FfxivColor = ffxivColor; } @@ -187,9 +223,7 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom } public VertexMaterialDelta Subtract(IVertexMaterial baseValue) - { - return new VertexMaterialDelta(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); - } + => new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1)); public Vector2 GetTexCoord(int index) => index switch @@ -201,9 +235,12 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom 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)); + 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) @@ -230,12 +267,17 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom => throw new ArgumentOutOfRangeException(nameof(index)); public void SetColor(int setIndex, Vector4 color) - { - } + { } public void Validate() { - var components = new[] { FfxivColor.X, FfxivColor.Y, FfxivColor.Z, FfxivColor.W }; + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; if (components.Any(component => component < 0 || component > 1)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index e81bb622..df08eea3 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -61,7 +61,7 @@ public class SubMeshImporter try { - _morphNames = node.Mesh.Extras.GetNode("targetNames").Deserialize>(); + _morphNames = node.Mesh.Extras["targetNames"].Deserialize>(); } catch { diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index ba089662..fed06573 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -148,7 +148,7 @@ public partial class TexToolsImporter : IDisposable // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry private static ZipArchiveEntry? FindZipEntry(ZipArchive file, string fileName) - => file.Entries.FirstOrDefault(e => !e.IsDirectory && e.Key.Contains(fileName)); + => file.Entries.FirstOrDefault(e => e is { IsDirectory: false, Key: not null } && e.Key.Contains(fileName)); private static string GetStringFromZipEntry(ZipArchiveEntry entry, Encoding encoding) { diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index dea343c6..febbe179 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -82,7 +82,7 @@ public partial class TexToolsImporter if (name.Length == 0) throw new Exception("Invalid mod archive: mod meta has no name."); - using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key)); + using var f = File.OpenWrite(Path.Combine(_currentModDirectory.FullName, reader.Entry.Key!)); s.Seek(0, SeekOrigin.Begin); s.WriteTo(f); } @@ -155,13 +155,9 @@ public partial class TexToolsImporter ret = directory; // Check that all other files are also contained in the top-level directory. - if (ret.IndexOfAny(new[] - { - '/', - '\\', - }) - >= 0 - || !archive.Entries.All(e => e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\'))) + if (ret.IndexOfAny(['/', '\\']) >= 0 + || !archive.Entries.All(e + => e.Key != null && e.Key.StartsWith(ret) && (e.Key.Length == ret.Length || e.Key[ret.Length] is '/' or '\\'))) throw new Exception( "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too."); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 8e143e3c..f42d16ad 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -86,11 +86,11 @@ - + - - - + + + diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 9041fbd0..aa2d445e 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -242,7 +242,7 @@ public class MigrationManager(Configuration config) : IService return; } - var path = Path.Combine(directory, reader.Entry.Key); + var path = Path.Combine(directory, reader.Entry.Key!); using var s = new MemoryStream(); using var e = reader.OpenEntryStream(); e.CopyTo(s); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 8e7106dd..fd3a0a9e 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -4,11 +4,11 @@ "net8.0-windows7.0": { "EmbedIO": { "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "YM6hpZNAfvbbixfG9T4lWDGfF0D/TqutbTROL4ogVcHKwPF1hp+xS3ABwd3cxxTxvDFkj/zZl57QgWuFA8Igxw==", + "requested": "[3.5.2, )", + "resolved": "3.5.2", + "contentHash": "YU4j+3XvuO8/VPkNf7KWOF1TpMhnyVhXnPsG1mvnDhTJ9D5BZOFXVDvCpE/SkQ1AJ0Aa+dXOVSW3ntgmLL7aJg==", "dependencies": { - "Unosquare.Swan.Lite": "3.0.0" + "Unosquare.Swan.Lite": "3.1.0" } }, "PeNet": { @@ -23,23 +23,26 @@ }, "SharpCompress": { "type": "Direct", - "requested": "[0.33.0, )", - "resolved": "0.33.0", - "contentHash": "FlHfpTAADzaSlVCBF33iKJk9UhOr3Xj+r5LXbW2GzqYr0SrhiOf6shLX2LC2fqs7g7d+YlwKbBXqWFtb+e7icw==" + "requested": "[0.37.2, )", + "resolved": "0.37.2", + "contentHash": "cFBpTct57aubLQXkdqMmgP8GGTFRh7fnRWP53lgE/EYUpDZJ27SSvTkdjB4OYQRZ20SJFpzczUquKLbt/9xkhw==", + "dependencies": { + "ZstdSharp.Port": "0.8.0" + } }, "SharpGLTF.Core": { "type": "Direct", - "requested": "[1.0.0-alpha0030, )", - "resolved": "1.0.0-alpha0030", - "contentHash": "HVL6PcrM0H/uEk96nRZfhtPeYvSFGHnni3g1aIckot2IWVp0jLMH5KWgaWfsatEz4Yds3XcdSLUWmJZivDBUPA==" + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w==" }, "SharpGLTF.Toolkit": { "type": "Direct", - "requested": "[1.0.0-alpha0030, )", - "resolved": "1.0.0-alpha0030", - "contentHash": "nsoJWAFhXgEky9bVCY0zLeZVDx+S88u7VjvuebvMb6dJiNyFOGF6FrrMHiJe+x5pcVBxxlc3VoXliBF7r/EqYA==", + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==", "dependencies": { - "SharpGLTF.Runtime": "1.0.0-alpha0030" + "SharpGLTF.Runtime": "1.0.1" } }, "SixLabors.ImageSharp": { @@ -50,8 +53,8 @@ }, "JetBrains.Annotations": { "type": "Transitive", - "resolved": "2023.3.0", - "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA==" + "resolved": "2024.2.0", + "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", @@ -73,10 +76,10 @@ }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.0-alpha0030", - "contentHash": "Ysn+fyj9EVXj6mfG0BmzSTBGNi/QvcnTrMd54dBMOlI/TsMRvnOY3JjTn0MpeH2CgHXX4qogzlDt4m+rb3n4Og==", + "resolved": "1.0.1", + "contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==", "dependencies": { - "SharpGLTF.Core": "1.0.0-alpha0030" + "SharpGLTF.Core": "1.0.1" } }, "System.Formats.Asn1": { @@ -99,16 +102,21 @@ }, "Unosquare.Swan.Lite": { "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==", + "resolved": "3.1.0", + "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==", "dependencies": { "System.ValueTuple": "4.5.0" } }, + "ZstdSharp.Port": { + "type": "Transitive", + "resolved": "0.8.0", + "contentHash": "Z62eNBIu8E8YtbqlMy57tK3dV1+m2b9NhPeaYovB5exmLKvrGCqOhJTzrEUH5VyUWU6vwX3c1XHJGhW5HVs8dA==" + }, "ottergui": { "type": "Project", "dependencies": { - "JetBrains.Annotations": "[2023.3.0, )", + "JetBrains.Annotations": "[2024.2.0, )", "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" } }, @@ -122,7 +130,7 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.2.0, )", + "Penumbra.Api": "[5.3.0, )", "Penumbra.String": "[1.0.4, )" } }, From 726340e4f8cc4a8ab22f9629e5362a800f3ae962 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 24 Aug 2024 20:45:18 +0200 Subject: [PATCH 367/865] Meh. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c8708ec5..c43c5cac 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c8708ec5153cb60c9e43b2c53d02b81b2c8175f9 +Subproject commit c43c5cac4cee092bf0aed8d46bab112b037ef8f2 From f3346c5d7e52f1ba86332ee2e30f77f144bf9ee3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Aug 2024 18:20:29 +0200 Subject: [PATCH 368/865] Add Targa support. --- Penumbra/Import/Textures/Texture.cs | 15 ++++++++++++ Penumbra/Import/Textures/TextureDrawer.cs | 2 +- Penumbra/Import/Textures/TextureManager.cs | 23 +++++++++++-------- .../AdvancedWindow/ModEditWindow.Textures.cs | 1 + 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index c5207e94..ae0aabd9 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -10,6 +10,21 @@ public enum TextureType Tex, Png, Bitmap, + Targa, +} + +internal static class TextureTypeExtensions +{ + public static TextureType ReduceToBehaviour(this TextureType type) + => type switch + { + TextureType.Dds => TextureType.Dds, + TextureType.Tex => TextureType.Tex, + TextureType.Png => TextureType.Png, + TextureType.Bitmap => TextureType.Png, + TextureType.Targa => TextureType.Png, + _ => TextureType.Unknown, + }; } public sealed class Texture : IDisposable diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index c83604e4..b0a65ac0 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -66,7 +66,7 @@ public static class TextureDrawer current.Load(textures, paths[0]); } - fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false); + fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false); } ImGui.SameLine(); diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index cc785d02..4afc8a56 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -165,11 +165,13 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur return; } + var imageTypeBehaviour = image.Type.ReduceToBehaviour(); var dds = _type switch { - CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, + CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, + cancel, rgba, width, height), - CombinedTexture.TextureSaveType.AsIs when image.Type is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), + CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), @@ -218,7 +220,9 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur => Path.GetExtension(path).ToLowerInvariant() switch { ".dds" => (LoadDds(path), TextureType.Dds), - ".png" => (LoadPng(path), TextureType.Png), + ".png" => (LoadImageSharp(path), TextureType.Png), + ".tga" => (LoadImageSharp(path), TextureType.Targa), + ".bmp" => (LoadImageSharp(path), TextureType.Bitmap), ".tex" => (LoadTex(path), TextureType.Tex), _ => throw new Exception($"Extension {Path.GetExtension(path)} unknown."), }; @@ -234,17 +238,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public BaseImage LoadDds(string path) => ScratchImage.LoadDDS(path); - /// Load a .png file from drive using ImageSharp. - public BaseImage LoadPng(string path) + /// Load a supported file type from drive using ImageSharp. + public BaseImage LoadImageSharp(string path) { using var stream = File.OpenRead(path); return Image.Load(stream); } - /// Convert an existing image to .png. Does not create a deep copy of an existing .png and just returns the existing one. + /// Convert an existing image to ImageSharp. Does not create a deep copy of an existing ImageSharp file and just returns the existing one. public static BaseImage ConvertToPng(BaseImage input, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { - switch (input.Type) + switch (input.Type.ReduceToBehaviour()) { case TextureType.Png: return input; case TextureType.Dds: @@ -261,7 +265,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public static BaseImage ConvertToRgbaDds(BaseImage input, bool mipMaps, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { - switch (input.Type) + switch (input.Type.ReduceToBehaviour()) { case TextureType.Png: { @@ -291,7 +295,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { - switch (input.Type) + switch (input.Type.ReduceToBehaviour()) { case TextureType.Png: { @@ -470,6 +474,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Png => $"Custom {_width} x {_height} .png Image", + TextureType.Targa => $"Custom {_width} x {_height} .tga Image", TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", _ => "Unknown Image", }; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 652ecb49..67a27a0b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -329,5 +329,6 @@ public partial class ModEditWindow ".png", ".dds", ".tex", + ".tga", }; } From c4853434c8842ee8fa390e5b716134eca407dbfd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Aug 2024 18:25:43 +0200 Subject: [PATCH 369/865] Whatever. --- Penumbra/Import/Textures/TextureManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 4afc8a56..996b5dbf 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -474,7 +474,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur TextureType.Dds => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Tex => $"Custom {_width} x {_height} {_image.Format} Image", TextureType.Png => $"Custom {_width} x {_height} .png Image", - TextureType.Targa => $"Custom {_width} x {_height} .tga Image", + TextureType.Targa => $"Custom {_width} x {_height} .tga Image", TextureType.Bitmap => $"Custom {_width} x {_height} RGBA Image", _ => "Unknown Image", }; From ded910d8a128b7ffb9cc9483c201df847a2b9e4c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 26 Aug 2024 21:21:38 +0200 Subject: [PATCH 370/865] Add Targa export. --- Penumbra.Api | 2 +- Penumbra/Api/Api/EditingApi.cs | 2 + Penumbra/Import/Textures/CombinedTexture.cs | 12 ++++ Penumbra/Import/Textures/TextureManager.cs | 56 ++++++++++++++----- .../AdvancedWindow/ModEditWindow.Textures.cs | 17 +++--- 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 552246e5..a38e9bcf 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 552246e595ffab2aaba2c75f578d564f8938fc9a +Subproject commit a38e9bcfb80c456102945bbb4c59f5621cae0442 diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs index 93345053..e50b7a1b 100644 --- a/Penumbra/Api/Api/EditingApi.cs +++ b/Penumbra/Api/Api/EditingApi.cs @@ -10,6 +10,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA => textureType switch { TextureType.Png => textureManager.SavePng(inputFile, outputFile), + TextureType.Targa => textureManager.SaveTga(inputFile, outputFile), TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, inputFile, outputFile), TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, inputFile, outputFile), TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, inputFile, outputFile), @@ -26,6 +27,7 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA => textureType switch { TextureType.Png => textureManager.SavePng(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Targa => textureManager.SaveTga(new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.AsIsTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.AsIsDds => textureManager.SaveAs(CombinedTexture.TextureSaveType.AsIs, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.RgbaTex => textureManager.SaveAs(CombinedTexture.TextureSaveType.Bitmap, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index 98b87ac3..c1a22088 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -55,6 +55,14 @@ public partial class CombinedTexture : IDisposable SaveTask = textures.SavePng(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); } + public void SaveAsTarga(TextureManager textures, string path) + { + if (!IsLoaded || _current == null) + return; + + SaveTask = textures.SaveTga(_current.BaseImage, path, _current.RgbaPixels, _current.TextureWrap!.Width, _current.TextureWrap!.Height); + } + private void SaveAs(TextureManager textures, string path, TextureSaveType type, bool mipMaps, bool writeTex) { if (!IsLoaded || _current == null) @@ -72,6 +80,7 @@ public partial class CombinedTexture : IDisposable ".tex" => TextureType.Tex, ".dds" => TextureType.Dds, ".png" => TextureType.Png, + ".tga" => TextureType.Targa, _ => TextureType.Unknown, }; @@ -85,6 +94,9 @@ public partial class CombinedTexture : IDisposable break; case TextureType.Png: SaveAsPng(textures, path); + break; + case TextureType.Targa: + SaveAsTarga(textures, path); break; default: throw new ArgumentException( diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 996b5dbf..7118f8af 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -8,6 +8,7 @@ using OtterGui.Tasks; using OtterTex; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -33,10 +34,17 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } public Task SavePng(string input, string output) - => Enqueue(new SavePngAction(this, input, output)); + => Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Png)); public Task SavePng(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SavePngAction(this, image, path, rgba, width, height)); + => Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Png, rgba, width, height)); + + public Task SaveTga(string input, string output) + => Enqueue(new SaveImageSharpAction(this, input, output, TextureType.Targa)); + + public Task SaveTga(BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + => Enqueue(new SaveImageSharpAction(this, image, path, TextureType.Targa, rgba, width, height)); + public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); @@ -66,44 +74,65 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur return t; } - private class SavePngAction : IAction + private class SaveImageSharpAction : IAction { private readonly TextureManager _textures; private readonly string _outputPath; private readonly ImageInputData _input; + private readonly TextureType _type; - public SavePngAction(TextureManager textures, string input, string output) + public SaveImageSharpAction(TextureManager textures, string input, string output, TextureType type) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; + _type = type; + if (_type.ReduceToBehaviour() is not TextureType.Png) + throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp."); } - public SavePngAction(TextureManager textures, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveImageSharpAction(TextureManager textures, BaseImage image, string path, TextureType type, byte[]? rgba = null, int width = 0, + int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; + _type = type; + if (_type.ReduceToBehaviour() is not TextureType.Png) + throw new ArgumentOutOfRangeException(nameof(type), type, $"Can not save as {type} with ImageSharp."); } public void Execute(CancellationToken cancel) { - _textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as .png to {_outputPath}..."); + _textures._logger.Information($"[{nameof(TextureManager)}] Saving {_input} as {_type} to {_outputPath}..."); var (image, rgba, width, height) = _input.GetData(_textures); cancel.ThrowIfCancellationRequested(); - Image? png = null; + Image? data = null; if (image.Type is TextureType.Unknown) { if (rgba != null && width > 0 && height > 0) - png = ConvertToPng(rgba, width, height).AsPng!; + data = ConvertToPng(rgba, width, height).AsPng!; } else { - png = ConvertToPng(image, cancel, rgba).AsPng!; + data = ConvertToPng(image, cancel, rgba).AsPng!; } cancel.ThrowIfCancellationRequested(); - png?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel).Wait(cancel); + switch (_type) + { + case TextureType.Png: + data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) + .Wait(cancel); + return; + case TextureType.Targa: + data?.SaveAsync(_outputPath, new TgaEncoder() + { + Compression = TgaCompression.None, + BitsPerPixel = TgaBitsPerPixel.Pixel32, + }, cancel).Wait(cancel); + return; + } } public override string ToString() @@ -111,7 +140,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public bool Equals(IAction? other) { - if (other is not SavePngAction rhs) + if (other is not SaveImageSharpAction rhs) return false; return string.Equals(_outputPath, rhs._outputPath, StringComparison.OrdinalIgnoreCase) && _input.Equals(rhs._input); @@ -168,9 +197,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur var imageTypeBehaviour = image.Type.ReduceToBehaviour(); var dds = _type switch { - CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, - cancel, rgba, - width, height), + CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Png => ConvertToRgbaDds(image, _mipMaps, cancel, + rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 67a27a0b..c08e8a8e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -85,7 +85,7 @@ public partial class ModEditWindow ImGuiUtil.SelectableHelpMarker(newDesc); } - } + } private void RedrawOnSaveBox() { @@ -128,7 +128,8 @@ public partial class ModEditWindow ? "This saves the texture in place. This is not revertible." : $"This saves the texture in place. This is not revertible. Hold {_config.DeleteModModifier} to save."; - var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var buttonSize2 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); if (ImGuiUtil.DrawDisabledButton("Save in place", buttonSize2, tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { @@ -141,17 +142,18 @@ public partial class ModEditWindow if (ImGui.Button("Save as TEX", buttonSize2)) OpenSaveAsDialog(".tex"); - if (ImGui.Button("Export as PNG", buttonSize2)) + if (ImGui.Button("Export as TGA", buttonSize3)) + OpenSaveAsDialog(".tga"); + ImGui.SameLine(); + if (ImGui.Button("Export as PNG", buttonSize3)) OpenSaveAsDialog(".png"); ImGui.SameLine(); - if (ImGui.Button("Export as DDS", buttonSize2)) + if (ImGui.Button("Export as DDS", buttonSize3)) OpenSaveAsDialog(".dds"); - ImGui.NewLine(); var canConvertInPlace = canSaveInPlace && _left.Type is TextureType.Tex && _center.IsLeftCopy; - var buttonSize3 = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X * 2) / 3, 0); if (ImGuiUtil.DrawDisabledButton("Convert to BC7", buttonSize3, "This converts the texture to BC7 format in place. This is not revertible.", !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) @@ -226,7 +228,8 @@ public partial class ModEditWindow private void OpenSaveAsDialog(string defaultExtension) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); - _fileDialog.OpenSavePicker("Save Texture as TEX, DDS or PNG...", "Textures{.png,.dds,.tex},.tex,.dds,.png", fileName, defaultExtension, + _fileDialog.OpenSavePicker("Save Texture as TEX, DDS, PNG or TGA...", "Textures{.png,.dds,.tex,.tga},.tex,.dds,.png,.tga", fileName, + defaultExtension, (a, b) => { if (a) From 3e2c9177a71ecd9a64493b02359b7ba16188c651 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 15:48:02 +0200 Subject: [PATCH 371/865] Prepare API for new meta format. --- Penumbra/Api/Api/MetaApi.cs | 258 ++++++++++++++++++++++++++++++- Penumbra/Api/Api/TemporaryApi.cs | 26 +--- 2 files changed, 257 insertions(+), 27 deletions(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index ce1a9def..6f3ed51e 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -1,16 +1,21 @@ +using Dalamud.Plugin.Services; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; using Penumbra.Meta.Manipulations; namespace Penumbra.Api.Api; -public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService +public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers) + : IPenumbraApiMeta, IApiService { - public const int CurrentVersion = 0; + public const int CurrentVersion = 1; public string GetPlayerMetaManipulations() { @@ -24,7 +29,32 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) return CompressMetaManipulations(collection); } + public Task GetPlayerMetaManipulationsAsync() + { + return Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(collectionResolver.PlayerCollection).ConfigureAwait(false); + return CompressMetaManipulations(playerCollection); + }); + } + + public Task GetMetaManipulationsAsync(int gameObjectIdx) + { + return Task.Run(async () => + { + var playerCollection = await framework.RunOnFrameworkThread(() => + { + helpers.AssociatedCollection(gameObjectIdx, out var collection); + return collection; + }).ConfigureAwait(false); + return CompressMetaManipulations(playerCollection); + }); + } + internal static string CompressMetaManipulations(ModCollection collection) + => CompressMetaManipulationsV0(collection); + + private static string CompressMetaManipulationsV0(ModCollection collection) { var array = new JArray(); if (collection.MetaCache is { } cache) @@ -38,6 +68,228 @@ public class MetaApi(CollectionResolver collectionResolver, ApiHelpers helpers) MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } - return Functions.ToCompressedBase64(array, CurrentVersion); + return Functions.ToCompressedBase64(array, 0); + } + + private static unsafe string CompressMetaManipulationsV1(ModCollection? collection) + { + using var ms = new MemoryStream(); + ms.Capacity = 1024; + using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true)) + { + zipStream.Write((byte)1); + zipStream.Write("META0001"u8); + if (collection?.MetaCache is not { } cache) + { + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + zipStream.Write(0); + } + else + { + WriteCache(zipStream, cache.Imc); + WriteCache(zipStream, cache.Eqp); + WriteCache(zipStream, cache.Eqdp); + WriteCache(zipStream, cache.Est); + WriteCache(zipStream, cache.Rsp); + WriteCache(zipStream, cache.Gmp); + cache.GlobalEqp.EnterReadLock(); + + try + { + zipStream.Write(cache.GlobalEqp.Count); + foreach (var (globalEqp, _) in cache.GlobalEqp) + zipStream.Write(new ReadOnlySpan(&globalEqp, sizeof(GlobalEqpManipulation))); + } + finally + { + cache.GlobalEqp.ExitReadLock(); + } + } + } + + ms.Flush(); + ms.Position = 0; + var data = ms.GetBuffer().AsSpan(0, (int)ms.Length); + return Convert.ToBase64String(data); + + void WriteCache(Stream stream, MetaCacheBase metaCache) + where TKey : unmanaged, IMetaIdentifier + where TValue : unmanaged + { + metaCache.EnterReadLock(); + try + { + stream.Write(metaCache.Count); + foreach (var (identifier, (_, value)) in metaCache) + { + stream.Write(identifier); + stream.Write(value); + } + } + finally + { + metaCache.ExitReadLock(); + } + } + } + + /// + /// Convert manipulations from a transmitted base64 string to actual manipulations. + /// The empty string is treated as an empty set. + /// Only returns true if all conversions are successful and distinct. + /// + internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) + { + if (manipString.Length == 0) + { + manips = new MetaDictionary(); + return true; + } + + try + { + var bytes = Convert.FromBase64String(manipString); + using var compressedStream = new MemoryStream(bytes); + using var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var resultStream = new MemoryStream(); + zipStream.CopyTo(resultStream); + resultStream.Flush(); + resultStream.Position = 0; + var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length); + var version = data[0]; + data = data[1..]; + switch (version) + { + case 0: return ConvertManipsV0(data, out manips); + case 1: return ConvertManipsV1(data, out manips); + default: + Penumbra.Log.Debug($"Invalid version for manipulations: {version}."); + manips = null; + return false; + } + } + catch (Exception ex) + { + Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}"); + manips = null; + return false; + } + } + + private static bool ConvertManipsV1(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + if (!data.StartsWith("META0001"u8)) + { + Penumbra.Log.Debug($"Invalid manipulations of version 1, does not start with valid prefix."); + manips = null; + return false; + } + + manips = new MetaDictionary(); + var r = new SpanBinaryReader(data[8..]); + var imcCount = r.ReadInt32(); + for (var i = 0; i < imcCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var eqpCount = r.ReadInt32(); + for (var i = 0; i < eqpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var eqdpCount = r.ReadInt32(); + for (var i = 0; i < eqdpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var estCount = r.ReadInt32(); + for (var i = 0; i < estCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var rspCount = r.ReadInt32(); + for (var i = 0; i < rspCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var gmpCount = r.ReadInt32(); + for (var i = 0; i < gmpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var globalEqpCount = r.ReadInt32(); + for (var i = 0; i < globalEqpCount; ++i) + { + var manip = r.Read(); + if (!manip.Validate() || !manips.TryAdd(manip)) + return false; + } + + return true; + } + + private static bool ConvertManipsV0(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + var json = Encoding.UTF8.GetString(data); + manips = JsonConvert.DeserializeObject(json); + return manips != null; + } + + internal void TestMetaManipulations() + { + var collection = collectionResolver.PlayerCollection(); + var dict = new MetaDictionary(collection.MetaCache); + var count = dict.Count; + + var watch = Stopwatch.StartNew(); + var v0 = CompressMetaManipulationsV0(collection); + var v0Time = watch.ElapsedMilliseconds; + + watch.Restart(); + var v1 = CompressMetaManipulationsV1(collection); + var v1Time = watch.ElapsedMilliseconds; + + watch.Restart(); + var v1Success = ConvertManips(v1, out var v1Roundtrip); + var v1RoundtripTime = watch.ElapsedMilliseconds; + + watch.Restart(); + var v0Success = ConvertManips(v0, out var v0Roundtrip); + var v0RoundtripTime = watch.ElapsedMilliseconds; + + Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal"); + Penumbra.Log.Information( + $"0 | {count} | {v0Time} | {v0.Length} | {v0Success} | {v0Roundtrip?.Count} | {v0RoundtripTime} | {v0Roundtrip?.Equals(dict)}"); + Penumbra.Log.Information( + $"1 | {count} | {v1Time} | {v1.Length} | {v1Success} | {v1Roundtrip?.Count} | {v1RoundtripTime} | {v0Roundtrip?.Equals(dict)}"); } } diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index f02b0d94..516b4347 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -1,10 +1,8 @@ -using OtterGui; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; using Penumbra.String.Classes; @@ -62,7 +60,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch @@ -88,7 +86,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch @@ -153,24 +151,4 @@ public class TemporaryApi( return true; } - - /// - /// Convert manipulations from a transmitted base64 string to actual manipulations. - /// The empty string is treated as an empty set. - /// Only returns true if all conversions are successful and distinct. - /// - private static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) - { - if (manipString.Length == 0) - { - manips = new MetaDictionary(); - return true; - } - - if (Functions.FromCompressedBase64(manipString, out manips!) == MetaApi.CurrentVersion) - return true; - - manips = null; - return false; - } } From 233a9996507521c6a184743cc2b0314d73ac427b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 15:48:42 +0200 Subject: [PATCH 372/865] Add button to remove default-valued meta entries. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 64 ++++++++++++++++++- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 15 +++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index bacf4122..d9018ff6 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,12 +1,15 @@ using System.Collections.Frozen; using OtterGui.Services; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService +public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManager, ImcChecker imcChecker) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -62,6 +65,65 @@ public class ModMetaEditor(ModManager modManager) : MetaDictionary, IService Changes = false; } + public void DeleteDefaultValues() + { + var clone = Clone(); + Clear(); + foreach (var (key, value) in clone.Imc) + { + var defaultEntry = imcChecker.GetDefaultEntry(key, false); + if (!defaultEntry.Entry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Eqp) + { + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Eqdp) + { + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Est) + { + var defaultEntry = EstFile.GetDefault(metaFileManager, key); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Gmp) + { + var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + + foreach (var (key, value) in clone.Rsp) + { + var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute); + if (!defaultEntry.Equals(value)) + TryAdd(key, value); + else + Changes = true; + } + } + public void Apply(IModDataContainer container) { if (!Changes) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 3ec6a4d5..49eac96e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -16,21 +16,21 @@ public partial class ModEditWindow private void DrawMetaTab() { - using var tab = ImRaii.TabItem("Meta Manipulations"); + using var tab = ImUtf8.TabItem("Meta Manipulations"u8); if (!tab) return; DrawOptionSelectHeader(); var setsEqual = !_editor.MetaEditor.Changes; - var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + var tt = setsEqual ? "No changes staged."u8 : "Apply the currently staged changes to the option."u8; ImGui.NewLine(); - if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) + if (ImUtf8.ButtonEx("Apply Changes"u8, tt, Vector2.Zero, setsEqual)) _editor.MetaEditor.Apply(_editor.Option!); ImGui.SameLine(); - tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) + tt = setsEqual ? "No changes staged."u8 : "Revert all currently staged changes."u8; + if (ImUtf8.ButtonEx("Revert Changes"u8, tt, Vector2.Zero, setsEqual)) _editor.MetaEditor.Load(_editor.Mod!, _editor.Option!); ImGui.SameLine(); @@ -40,8 +40,11 @@ public partial class ModEditWindow ImGui.SameLine(); CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor); ImGui.SameLine(); - if (ImGui.Button("Write as TexTools Files")) + if (ImUtf8.Button("Write as TexTools Files"u8)) _metaFileManager.WriteAllTexToolsMeta(Mod!); + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Remove All Default-Values", "Delete any entries from all lists that set the value to its default value."u8)) + _editor.MetaEditor.DeleteDefaultValues(); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) From a3c22f2826b4091010e6f76ad5a236e1c92b44fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 15:49:07 +0200 Subject: [PATCH 373/865] Fix ordering of meta entries. --- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index f586045c..aea2ef78 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -61,7 +61,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil } protected override IEnumerable<(EqdpIdentifier, EqdpEntryInternal)> Enumerate() - => Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId) + => Editor.Eqdp.OrderBy(kvp => kvp.Key.SetId.Id) .ThenBy(kvp => kvp.Key.GenderRace) .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index b1031b44..733517f3 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -60,7 +60,7 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(EqpIdentifier, EqpEntryInternal)> Enumerate() => Editor.Eqp - .OrderBy(kvp => kvp.Key.SetId) + .OrderBy(kvp => kvp.Key.SetId.Id) .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 628cee40..a33f8b7b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -59,7 +59,7 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(EstIdentifier, EstEntry)> Enumerate() => Editor.Est - .OrderBy(kvp => kvp.Key.SetId) + .OrderBy(kvp => kvp.Key.SetId.Id) .ThenBy(kvp => kvp.Key.GenderRace) .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index bc2e1bde..5d67ddcf 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -49,7 +49,7 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me protected override IEnumerable<(GlobalEqpManipulation, byte)> Enumerate() => Editor.GlobalEqp .OrderBy(identifier => identifier.Type) - .ThenBy(identifier => identifier.Condition) + .ThenBy(identifier => identifier.Condition.Id) .Select(identifier => (identifier, (byte)0)); private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 1e91731d..bd42b60a 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -58,7 +58,7 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() => Editor.Gmp - .OrderBy(kvp => kvp.Key.SetId) + .OrderBy(kvp => kvp.Key.SetId.Id) .Select(kvp => (kvp.Key, kvp.Value)); private static bool DrawIdentifierInput(ref GmpIdentifier identifier) diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index e33eb1aa..4e949b98 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -142,11 +142,11 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(ImcIdentifier, ImcEntry)> Enumerate() => Editor.Imc .OrderBy(kvp => kvp.Key.ObjectType) - .ThenBy(kvp => kvp.Key.PrimaryId) + .ThenBy(kvp => kvp.Key.PrimaryId.Id) .ThenBy(kvp => kvp.Key.EquipSlot) .ThenBy(kvp => kvp.Key.BodySlot) - .ThenBy(kvp => kvp.Key.SecondaryId) - .ThenBy(kvp => kvp.Key.Variant) + .ThenBy(kvp => kvp.Key.SecondaryId.Id) + .ThenBy(kvp => kvp.Key.Variant.Id) .Select(kvp => (kvp.Key, kvp.Value)); public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) From d713d5a112d138d43fc6d4470711cd8d1c711631 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:28:49 +0200 Subject: [PATCH 374/865] Improve handling of mod selection. --- Penumbra/Communication/CollectionChange.cs | 3 + .../CollectionInheritanceChanged.cs | 3 + Penumbra/Communication/ModSettingChanged.cs | 4 +- Penumbra/Mods/Editor/ModMerger.cs | 39 ++++--- Penumbra/Mods/ModSelection.cs | 104 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 7 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 18 +-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 82 ++++---------- Penumbra/UI/ModsTab/ModPanel.cs | 35 +++--- Penumbra/UI/ModsTab/ModPanelHeader.cs | 34 ++++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 43 ++++---- Penumbra/UI/Tabs/ModsTab.cs | 3 +- 12 files changed, 227 insertions(+), 148 deletions(-) create mode 100644 Penumbra/Mods/ModSelection.cs diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 95d4ac4d..2788177d 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -46,5 +46,8 @@ public sealed class CollectionChange() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Communication/CollectionInheritanceChanged.cs b/Penumbra/Communication/CollectionInheritanceChanged.cs index dbcf9e4a..30af2b20 100644 --- a/Penumbra/Communication/CollectionInheritanceChanged.cs +++ b/Penumbra/Communication/CollectionInheritanceChanged.cs @@ -23,5 +23,8 @@ public sealed class CollectionInheritanceChanged() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Communication/ModSettingChanged.cs b/Penumbra/Communication/ModSettingChanged.cs index 7fda2f35..d4bf00be 100644 --- a/Penumbra/Communication/ModSettingChanged.cs +++ b/Penumbra/Communication/ModSettingChanged.cs @@ -1,5 +1,4 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -35,5 +34,8 @@ public sealed class ModSettingChanged() /// ModFileSystemSelector = 0, + + /// + ModSelection = 10, } } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index b059813b..d75ac671 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -10,22 +10,21 @@ using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.UI.ModsTab; namespace Penumbra.Mods.Editor; public class ModMerger : IDisposable, IService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly ModGroupEditor _editor; - private readonly ModFileSystemSelector _selector; - private readonly DuplicateManager _duplicates; - private readonly ModManager _mods; - private readonly ModCreator _creator; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly ModGroupEditor _editor; + private readonly ModSelection _selection; + private readonly DuplicateManager _duplicates; + private readonly ModManager _mods; + private readonly ModCreator _creator; public Mod? MergeFromMod - => _selector.Selected; + => _selection.Mod; public Mod? MergeToMod; public string OptionGroupName = "Merges"; @@ -41,23 +40,23 @@ public class ModMerger : IDisposable, IService public readonly IReadOnlyList Warnings = new List(); public Exception? Error { get; private set; } - public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { - _editor = editor; - _selector = selector; - _duplicates = duplicates; - _communicator = communicator; - _creator = creator; - _config = config; - _mods = mods; - _selector.SelectionChanged += OnSelectionChange; + _editor = editor; + _selection = selection; + _duplicates = duplicates; + _communicator = communicator; + _creator = creator; + _config = config; + _mods = mods; + _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); } public void Dispose() { - _selector.SelectionChanged -= OnSelectionChange; + _selection.Unsubscribe(OnSelectionChange); _communicator.ModPathChanged.Unsubscribe(OnModPathChange); } @@ -390,7 +389,7 @@ public class ModMerger : IDisposable, IService } } - private void OnSelectionChange(Mod? oldSelection, Mod? newSelection, in ModFileSystemSelector.ModState state) + private void OnSelectionChange(Mod? oldSelection, Mod? newSelection) { if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text) OptionName = newSelection?.Name.Text ?? string.Empty; diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs new file mode 100644 index 00000000..73d0272b --- /dev/null +++ b/Penumbra/Mods/ModSelection.cs @@ -0,0 +1,104 @@ +using OtterGui.Classes; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; +using Penumbra.Services; + +namespace Penumbra.Mods; + +/// +/// Triggered whenever the selected mod changes +/// +/// Parameter is the old selected mod. +/// Parameter is the new selected mod +/// +/// +public class ModSelection : EventWrapper +{ + private readonly ActiveCollections _collections; + private readonly EphemeralConfig _config; + private readonly CommunicatorService _communicator; + + public ModSelection(CommunicatorService communicator, ModManager mods, ActiveCollections collections, EphemeralConfig config) + : base(nameof(ModSelection)) + { + _communicator = communicator; + _collections = collections; + _config = config; + if (_config.LastModPath.Length > 0) + SelectMod(mods.FirstOrDefault(m => string.Equals(m.Identifier, config.LastModPath, StringComparison.OrdinalIgnoreCase))); + + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection); + _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection); + _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); + } + + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + + + public void SelectMod(Mod? mod) + { + if (mod == Mod) + return; + + var oldMod = Mod; + Mod = mod; + OnCollectionChange(CollectionType.Current, null, _collections.Current, string.Empty); + Invoke(oldMod, Mod); + _config.LastModPath = mod?.ModPath.Name ?? string.Empty; + _config.Save(); + } + + protected override void Dispose(bool _) + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.CollectionInheritanceChanged.Unsubscribe(OnInheritanceChange); + _communicator.ModSettingChanged.Unsubscribe(OnSettingChange); + } + + private void OnCollectionChange(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _2) + { + if (type is CollectionType.Current && oldCollection != newCollection) + UpdateSettings(); + } + + private void OnSettingChange(ModCollection collection, ModSettingChange _1, Mod? mod, Setting _2, int _3, bool _4) + { + if (collection == _collections.Current && mod == Mod) + UpdateSettings(); + } + + private void OnInheritanceChange(ModCollection collection, bool arg2) + { + if (collection == _collections.Current) + UpdateSettings(); + } + + private void UpdateSettings() + { + if (Mod == null) + { + Settings = ModSettings.Empty; + Collection = ModCollection.Empty; + } + else + { + (var settings, Collection) = _collections.Current[Mod.Index]; + Settings = settings ?? ModSettings.Empty; + } + } + + public enum Priority + { + /// + ModPanel = 0, + + /// + ModMerger = 0, + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 13458252..7bb067d8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -36,8 +36,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; - public readonly MigrationManager MigrationManager; - private readonly PerformanceTracker _performance; private readonly ModEditor _editor; private readonly Configuration _config; @@ -587,7 +585,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, ResourceTreeViewerFactory resourceTreeViewerFactory, IFramework framework, MetaDrawers metaDrawers, MigrationManager migrationManager, - MtrlTabFactory mtrlTabFactory) + MtrlTabFactory mtrlTabFactory, ModSelection selection) : base(WindowBaseLabel) { _performance = performance; @@ -604,7 +602,6 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _models = models; _fileDialog = fileDialog; _framework = framework; - MigrationManager = migrationManager; _metaDrawers = metaDrawers; _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, @@ -622,6 +619,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; + if (IsOpen && selection.Mod != null) + ChangeMod(selection.Mod); } public void Dispose() diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 0f9b2518..3972e350 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -5,24 +5,24 @@ using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; +using Penumbra.Mods; using Penumbra.UI.CollectionTab; -using Penumbra.UI.ModsTab; namespace Penumbra.UI.Classes; public class CollectionSelectHeader : IUiService { - private readonly CollectionCombo _collectionCombo; - private readonly ActiveCollections _activeCollections; - private readonly TutorialService _tutorial; - private readonly ModFileSystemSelector _selector; - private readonly CollectionResolver _resolver; + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModSelection _selection; + private readonly CollectionResolver _resolver; - public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModFileSystemSelector selector, + public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection, CollectionResolver resolver) { _tutorial = tutorial; - _selector = selector; + _selection = selection; _resolver = resolver; _activeCollections = collectionManager.Active; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); @@ -115,7 +115,7 @@ public class CollectionSelectHeader : IUiService private (ModCollection?, string, string, bool) GetInheritedCollectionInfo() { - var collection = _selector.Selected == null ? null : _selector.SelectedSettingCollection; + var collection = _selection.Mod == null ? null : _selection.Collection; return CheckCollection(collection, true) switch { CollectionState.Unavailable => (null, "Not Inherited", diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 42689efb..2f76340b 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -25,7 +25,6 @@ namespace Penumbra.UI.ModsTab; public sealed class ModFileSystemSelector : FileSystemSelector, IUiService { private readonly CommunicatorService _communicator; - private readonly MessageService _messager; private readonly Configuration _config; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; @@ -33,15 +32,12 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) - { - var mod = _modManager.FirstOrDefault(m - => string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase)); - if (mod != null) - SelectByValue(mod); - } - + if (_selection.Mod != null) + SelectByValue(_selection.Mod); _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector); _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector); _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector); _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModFileSystemSelector); _communicator.ModDiscoveryStarted.Subscribe(StoreCurrentSelection, ModDiscoveryStarted.Priority.ModFileSystemSelector); _communicator.ModDiscoveryFinished.Subscribe(RestoreLastSelection, ModDiscoveryFinished.Priority.ModFileSystemSelector); - OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, ""); - } - + SetFilterDirty(); + SelectionChanged += OnSelectionChanged; + } + public void SetRenameSearchPath(RenameField value) { switch (value) @@ -449,12 +439,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector _selection.SelectMod(newSelection); + #endregion #region Filters @@ -567,7 +529,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Appropriately identify and set the string filter and its type. protected override bool ChangeFilter(string filterValue) { - Filter.Parse(filterValue); + _filter.Parse(filterValue); return true; } @@ -597,7 +559,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Apply the string filters. private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) - => !Filter.IsVisible(leaf); + => !_filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index ee6fab1f..9d6ead62 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -10,22 +10,23 @@ namespace Penumbra.UI.ModsTab; public class ModPanel : IDisposable, IUiService { - private readonly MultiModPanel _multiModPanel; - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModPanelHeader _header; - private readonly ModPanelTabBar _tabs; - private bool _resetCursor; + private readonly MultiModPanel _multiModPanel; + private readonly ModSelection _selection; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; + private bool _resetCursor; - public ModPanel(IDalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs, + public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModEditWindow editWindow, ModPanelTabBar tabs, MultiModPanel multiModPanel, CommunicatorService communicator) { - _selector = selector; - _editWindow = editWindow; - _tabs = tabs; - _multiModPanel = multiModPanel; - _header = new ModPanelHeader(pi, communicator); - _selector.SelectionChanged += OnSelectionChange; + _selection = selection; + _editWindow = editWindow; + _tabs = tabs; + _multiModPanel = multiModPanel; + _header = new ModPanelHeader(pi, communicator); + _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel); + OnSelectionChange(null, _selection.Mod); } public void Draw() @@ -52,17 +53,17 @@ public class ModPanel : IDisposable, IUiService public void Dispose() { - _selector.SelectionChanged -= OnSelectionChange; + _selection.Unsubscribe(OnSelectionChange); _header.Dispose(); } private bool _valid; private Mod _mod = null!; - private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _) + private void OnSelectionChange(Mod? old, Mod? mod) { _resetCursor = true; - if (mod == null || _selector.Selected == null) + if (mod == null || _selection.Mod == null) { _editWindow.IsOpen = false; _valid = false; @@ -73,7 +74,7 @@ public class ModPanel : IDisposable, IUiService _editWindow.ChangeMod(mod); _valid = true; _mod = mod; - _header.UpdateModData(_mod); + _header.ChangeMod(_mod); _tabs.Settings.Reset(); _tabs.Edit.Reset(); } diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index 6c974f9c..aafbffa6 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -18,7 +18,8 @@ public class ModPanelHeader : IDisposable private readonly IFontHandle _nameFont; private readonly CommunicatorService _communicator; - private float _lastPreSettingsHeight = 0; + private float _lastPreSettingsHeight; + private bool _dirty = true; public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) { @@ -33,6 +34,7 @@ public class ModPanelHeader : IDisposable /// public void Draw() { + UpdateModData(); var height = ImGui.GetContentRegionAvail().Y; var maxHeight = 3 * height / 4; using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers @@ -49,16 +51,25 @@ public class ModPanelHeader : IDisposable _lastPreSettingsHeight = ImGui.GetCursorPosY(); } + public void ChangeMod(Mod mod) + { + _mod = mod; + _dirty = true; + } + /// /// Update all mod header data. Should someone change frame padding or item spacing, /// or his default font, this will break, but he will just have to select a different mod to restore. /// - public void UpdateModData(Mod mod) + private void UpdateModData() { + if (!_dirty) + return; + + _dirty = false; _lastPreSettingsHeight = 0; - _mod = mod; // Name - var name = $" {mod.Name} "; + var name = $" {_mod.Name} "; if (name != _modName) { using var f = _nameFont.Push(); @@ -67,16 +78,16 @@ public class ModPanelHeader : IDisposable } // Author - if (mod.Author != _modAuthor) + if (_mod.Author != _modAuthor) { - var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}"; - _modAuthor = mod.Author.Text; + var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}"; + _modAuthor = _mod.Author.Text; _modAuthorWidth = ImGui.CalcTextSize(author).X; _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; } // Version - var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty; + var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; if (version != _modVersion) { _modVersion = version; @@ -84,9 +95,9 @@ public class ModPanelHeader : IDisposable } // Website - if (_modWebsite != mod.Website) + if (_modWebsite != _mod.Website) { - _modWebsite = mod.Website; + _modWebsite = _mod.Website; _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; @@ -253,7 +264,6 @@ public class ModPanelHeader : IDisposable { const ModDataChangeType relevantChanges = ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version; - if ((changeType & relevantChanges) != 0) - UpdateModData(mod); + _dirty = (changeType & relevantChanges) != 0; } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 7e3b8a95..d2fbd0cd 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -3,9 +3,9 @@ using OtterGui.Raii; using OtterGui; using OtterGui.Services; using OtterGui.Widgets; -using Penumbra.Collections; using Penumbra.UI.Classes; using Penumbra.Collections.Manager; +using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Mods.Settings; @@ -16,16 +16,14 @@ namespace Penumbra.UI.ModsTab; public class ModPanelSettingsTab( CollectionManager collectionManager, ModManager modManager, - ModFileSystemSelector selector, + ModSelection selection, TutorialService tutorial, CommunicatorService communicator, ModGroupDrawer modGroupDrawer) : ITab, IUiService { - private bool _inherited; - private ModSettings _settings = null!; - private ModCollection _collection = null!; - private int? _currentPriority; + private bool _inherited; + private int? _currentPriority; public ReadOnlySpan Label => "Settings"u8; @@ -42,12 +40,10 @@ public class ModPanelSettingsTab( if (!child) return; - _settings = selector.SelectedSettings; - _collection = selector.SelectedSettingCollection; - _inherited = _collection != collectionManager.Active.Current; + _inherited = selection.Collection != collectionManager.Active.Current; DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier); + communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); DrawEnabledInput(); tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); @@ -55,11 +51,11 @@ public class ModPanelSettingsTab( tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); - communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier); + communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); - modGroupDrawer.Draw(selector.Selected!, _settings); + modGroupDrawer.Draw(selection.Mod!, selection.Settings); UiHelpers.DefaultLineSpace(); - communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier); + communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier); } /// Draw a big red bar if the current setting is inherited. @@ -70,8 +66,8 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); + if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width)) + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -80,12 +76,12 @@ public class ModPanelSettingsTab( /// Draw a checkbox for the enabled status of the mod. private void DrawEnabledInput() { - var enabled = _settings.Enabled; + var enabled = selection.Settings.Enabled; if (!ImGui.Checkbox("Enabled", ref enabled)) return; - modManager.SetKnown(selector.Selected!); - collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled); + modManager.SetKnown(selection.Mod!); + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); } /// @@ -95,15 +91,16 @@ public class ModPanelSettingsTab( private void DrawPriorityInput() { using var group = ImRaii.Group(); - var priority = _currentPriority ?? _settings.Priority.Value; + var settings = selection.Settings; + var priority = _currentPriority ?? settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); if (ImGui.InputInt("##Priority", ref priority, 0, 0)) _currentPriority = priority; if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { - if (_currentPriority != _settings.Priority.Value) - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + if (_currentPriority != settings.Priority.Value) + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, new ModPriority(_currentPriority.Value)); _currentPriority = null; @@ -120,13 +117,13 @@ public class ModPanelSettingsTab( private void DrawRemoveSettings() { const string text = "Inherit Settings"; - if (_inherited || _settings == ModSettings.Empty) + if (_inherited || selection.Settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true); + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 50fdc1d3..87338bdb 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -82,8 +82,7 @@ public class ModsTab( + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n" - + $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"); } } From 4970e571316fc6b6394b029e03bd26119fde98bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:29:12 +0200 Subject: [PATCH 375/865] Improve tooltip of file redirections tab. --- OtterGui | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 28 +++++++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/OtterGui b/OtterGui index 276327f8..9217ac56 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 276327f812e2f7e6aac7aee9e5ef0a560b065765 +Subproject commit 9217ac56697bc8285ced483b1fd4734fd36ba64d diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index ffa7473d..b07633b6 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,8 +1,10 @@ +using System.Linq; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; @@ -144,22 +146,20 @@ public partial class ModEditWindow private static string DrawFileTooltip(FileRegistry registry, ColorId color) { - (string, int) GetMulti() - { - var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); - return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length); - } - var (text, groupCount) = color switch { - ColorId.ConflictingMod => (string.Empty, 0), - ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1), + ColorId.ConflictingMod => (null, 0), + ColorId.NewMod => ([registry.SubModUsage[0].Item1.GetName()], 1), ColorId.InheritedMod => GetMulti(), - _ => (string.Empty, 0), + _ => (null, 0), }; - if (text.Length > 0 && ImGui.IsItemHovered()) - ImGui.SetTooltip(text); + if (text != null && ImGui.IsItemHovered()) + { + using var tt = ImUtf8.Tooltip(); + using var c = ImRaii.DefaultColors(); + ImUtf8.Text(string.Join('\n', text)); + } return (groupCount, registry.SubModUsage.Count) switch @@ -169,6 +169,12 @@ public partial class ModEditWindow (1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)", _ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)", }; + + (IEnumerable, int) GetMulti() + { + var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); + return (groups.Select(g => g.Key.GetName()), groups.Length); + } } private void DrawSelectable(FileRegistry registry) From 6d408ba695666e67be5d77d5387b676be839a744 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:29:47 +0200 Subject: [PATCH 376/865] Clip meta changes. --- Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 3 +++ .../UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 5 ++++- Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 3 +++ Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 15 +++++++++------ Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 3 +++ 8 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index aea2ef78..f9baddbe 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -66,6 +66,9 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Eqdp.Count; + private static bool DrawIdentifierInput(ref EqdpIdentifier identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 733517f3..51b14459 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -64,6 +64,9 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Eqp.Count; + private static bool DrawIdentifierInput(ref EqpIdentifier identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index a33f8b7b..09075319 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -64,6 +64,9 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Slot) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Est.Count; + private static bool DrawIdentifierInput(ref EstIdentifier identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 5d67ddcf..1aa9060e 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -52,6 +52,9 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me .ThenBy(identifier => identifier.Condition.Id) .Select(identifier => (identifier, (byte)0)); + protected override int Count + => Editor.GlobalEqp.Count; + private static void DrawIdentifierInput(ref GlobalEqpManipulation identifier) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index bd42b60a..9532d8e7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -59,7 +59,10 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override IEnumerable<(GmpIdentifier, GmpEntry)> Enumerate() => Editor.Gmp .OrderBy(kvp => kvp.Key.SetId.Id) - .Select(kvp => (kvp.Key, kvp.Value)); + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Gmp.Count; private static bool DrawIdentifierInput(ref GmpIdentifier identifier) { diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 4e949b98..53c61292 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -149,6 +149,9 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Variant.Id) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Imc.Count; + public static bool DrawObjectType(ref ImcIdentifier identifier, float width = 110) { var ret = Combos.ImcType("##imcType", identifier.ObjectType, out var type, width); diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 229526c4..75de20a7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -41,12 +41,14 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta using var id = ImUtf8.PushId((int)Identifier.Type); DrawNew(); - foreach (var ((identifier, entry), idx) in Enumerate().WithIndex()) - { - id.Push(idx); - DrawEntry(identifier, entry); - id.Pop(); - } + + var height = ImUtf8.FrameHeightSpacing; + var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); + var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); + ImGuiClip.DrawEndDummy(remainder, height); + + void DrawLine((TIdentifier Identifier, TEntry Value) pair) + => DrawEntry(pair.Identifier, pair.Value); } public abstract ReadOnlySpan Label { get; } @@ -57,6 +59,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); protected abstract IEnumerable<(TIdentifier, TEntry)> Enumerate(); + protected abstract int Count { get; } /// diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 6d819b16..87e8c5b8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -63,6 +63,9 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .ThenBy(kvp => kvp.Key.Attribute) .Select(kvp => (kvp.Key, kvp.Value)); + protected override int Count + => Editor.Rsp.Count; + private static bool DrawIdentifierInput(ref RspIdentifier identifier) { ImGui.TableNextColumn(); From 4117d45d152e9c6c3f29656c0f103f1884a5e38a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 18:32:22 +0200 Subject: [PATCH 377/865] Use ReadWriteDictionary as base for meta changes. --- OtterGui | 2 +- Penumbra/Collections/Cache/GlobalEqpCache.cs | 3 +- Penumbra/Collections/Cache/MetaCache.cs | 6 ++- .../Cache/{IMetaCache.cs => MetaCacheBase.cs} | 37 ++++++------------- 4 files changed, 20 insertions(+), 28 deletions(-) rename Penumbra/Collections/Cache/{IMetaCache.cs => MetaCacheBase.cs} (52%) diff --git a/OtterGui b/OtterGui index 9217ac56..bfbde4f8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9217ac56697bc8285ced483b1fd4734fd36ba64d +Subproject commit bfbde4f8aa6acc8eb3ed8bc419d5ae2afc77b5f1 diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index 1c80b47d..efcab109 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using OtterGui.Services; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; @@ -5,7 +6,7 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; -public class GlobalEqpCache : Dictionary, IService +public class GlobalEqpCache : ReadWriteDictionary, IService { private readonly HashSet _doNotHideEarrings = []; private readonly HashSet _doNotHideNecklace = []; diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 1a6924a9..05a94ac5 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -3,7 +3,6 @@ using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; -using Penumbra.String.Classes; namespace Penumbra.Collections.Cache; @@ -16,6 +15,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); + public bool IsDisposed { get; private set; } public int Count => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; @@ -42,6 +42,10 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public void Dispose() { + if (IsDisposed) + return; + + IsDisposed = true; Eqp.Dispose(); Eqdp.Dispose(); Est.Dispose(); diff --git a/Penumbra/Collections/Cache/IMetaCache.cs b/Penumbra/Collections/Cache/MetaCacheBase.cs similarity index 52% rename from Penumbra/Collections/Cache/IMetaCache.cs rename to Penumbra/Collections/Cache/MetaCacheBase.cs index fecc6f50..98a87e3f 100644 --- a/Penumbra/Collections/Cache/IMetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCacheBase.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; @@ -5,27 +6,19 @@ using Penumbra.Mods.Editor; namespace Penumbra.Collections.Cache; public abstract class MetaCacheBase(MetaFileManager manager, ModCollection collection) - : Dictionary + : ReadWriteDictionary where TIdentifier : unmanaged, IMetaIdentifier where TEntry : unmanaged { - protected readonly MetaFileManager Manager = manager; - protected readonly ModCollection Collection = collection; - - public void Dispose() - { - Dispose(true); - } + protected readonly MetaFileManager Manager = manager; + protected readonly ModCollection Collection = collection; public bool ApplyMod(IMod source, TIdentifier identifier, TEntry entry) { - lock (this) - { - if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry)) - return false; + if (TryGetValue(identifier, out var pair) && pair.Source == source && EqualityComparer.Default.Equals(pair.Entry, entry)) + return false; - this[identifier] = (source, entry); - } + this[identifier] = (source, entry); ApplyModInternal(identifier, entry); return true; @@ -33,17 +26,14 @@ public abstract class MetaCacheBase(MetaFileManager manager public bool RevertMod(TIdentifier identifier, [NotNullWhen(true)] out IMod? mod) { - lock (this) + if (!Remove(identifier, out var pair)) { - if (!Remove(identifier, out var pair)) - { - mod = null; - return false; - } - - mod = pair.Source; + mod = null; + return false; } + mod = pair.Source; + RevertModInternal(identifier); return true; } @@ -54,7 +44,4 @@ public abstract class MetaCacheBase(MetaFileManager manager protected virtual void RevertModInternal(TIdentifier identifier) { } - - protected virtual void Dispose(bool _) - { } } From f04331188252806b983adac72aa99134d36bc5c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 23:10:22 +0200 Subject: [PATCH 378/865] Fix vulnerability warning. --- Penumbra/Penumbra.csproj | 2 ++ Penumbra/packages.lock.json | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f42d16ad..9b613729 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -86,6 +86,8 @@ + + diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index fd3a0a9e..5b868212 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -51,6 +51,12 @@ "resolved": "3.1.5", "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" }, + "System.Formats.Asn1": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2024.2.0", @@ -82,11 +88,6 @@ "SharpGLTF.Core": "1.0.1" } }, - "System.Formats.Asn1": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AJukBuLoe3QeAF+mfaRKQb2dgyrvt340iMBHYv+VdBzCUM06IxGlvl0o/uPOS7lHnXPN6u8fFRHSHudx5aTi8w==" - }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", "resolved": "8.0.0", From f8e3b6777fd347ff5c19899fcf1d0b695486c901 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 Aug 2024 23:10:59 +0200 Subject: [PATCH 379/865] Add DeleteDefaultValues on general dicts. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 67 ++++++++++++++++++++------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index d9018ff6..6b5ec378 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,6 +1,5 @@ using System.Collections.Frozen; using OtterGui.Services; -using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -65,65 +64,101 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage Changes = false; } - public void DeleteDefaultValues() + public bool DeleteDefaultValues(MetaDictionary dict) { - var clone = Clone(); - Clear(); + var clone = dict.Clone(); + dict.Clear(); + var ret = false; foreach (var (key, value) in clone.Imc) { var defaultEntry = imcChecker.GetDefaultEntry(key, false); if (!defaultEntry.Entry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Eqp) { var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Eqdp) { var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Est) { var defaultEntry = EstFile.GetDefault(metaFileManager, key); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Gmp) { var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } foreach (var (key, value) in clone.Rsp) { var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute); if (!defaultEntry.Equals(value)) - TryAdd(key, value); + { + dict.TryAdd(key, value); + } else - Changes = true; + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ret = true; + } } + + return ret; } + public void DeleteDefaultValues() + => Changes = DeleteDefaultValues(this); + public void Apply(IModDataContainer container) { if (!Changes) From f5e61324627be7e192ce124d2a59154673adb857 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 17:47:51 +0200 Subject: [PATCH 380/865] Delete default meta entries from archives and api added mods if not configured otherwise. --- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/Mods/Editor/DuplicateManager.cs | 2 +- Penumbra/Mods/Editor/ModFileEditor.cs | 2 +- Penumbra/Mods/Editor/ModMerger.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 33 +++++---- Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Manager/ModImportManager.cs | 2 +- Penumbra/Mods/Manager/ModManager.cs | 10 +-- Penumbra/Mods/Manager/ModMigration.cs | 6 +- .../Manager/OptionEditor/ModGroupEditor.cs | 30 ++++---- Penumbra/Mods/ModCreator.cs | 71 +++++++++++-------- Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- 14 files changed, 95 insertions(+), 73 deletions(-) diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 2acdf031..31f20c5e 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -82,7 +82,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable != Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir.Parent.FullName))) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); - _modManager.AddMod(dir); + _modManager.AddMod(dir, true); if (_config.MigrateImportedModelsToV6) { _migrationManager.MigrateMdlDirectory(dir.FullName, false); diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index bcecf264..56a19766 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -225,7 +225,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co if (!useModManager || !modManager.TryGetMod(modDirectory.Name, string.Empty, out var mod)) { mod = new Mod(modDirectory); - modManager.Creator.ReloadMod(mod, true, out _); + modManager.Creator.ReloadMod(mod, true, true, out _); } Clear(); diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 55e0e94e..3b765215 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -151,7 +151,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu if (deletions <= 0) return; - modManager.Creator.ReloadMod(mod, false, out _); + modManager.Creator.ReloadMod(mod, false, false, out _); files.UpdateAll(mod, option); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index d75ac671..e3eb5f54 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -256,7 +256,7 @@ public class ModMerger : IDisposable, IService if (dir == null) throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); - _mods.AddMod(dir); + _mods.AddMod(dir, false); result = _mods[^1]; if (mods.Count == 1) { diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 6b5ec378..81a33db6 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -3,12 +3,15 @@ using OtterGui.Services; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Editor; -public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManager, ImcChecker imcChecker) : MetaDictionary, IService +public class ModMetaEditor( + ModGroupEditor groupEditor, + MetaFileManager metaFileManager, + ImcChecker imcChecker) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -64,11 +67,11 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage Changes = false; } - public bool DeleteDefaultValues(MetaDictionary dict) + public static bool DeleteDefaultValues(MetaFileManager metaFileManager, ImcChecker imcChecker, MetaDictionary dict) { var clone = dict.Clone(); dict.Clear(); - var ret = false; + var count = 0; foreach (var (key, value) in clone.Imc) { var defaultEntry = imcChecker.GetDefaultEntry(key, false); @@ -79,7 +82,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -93,7 +96,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -107,7 +110,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -121,7 +124,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -135,7 +138,7 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } @@ -149,22 +152,26 @@ public class ModMetaEditor(ModManager modManager, MetaFileManager metaFileManage else { Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); - ret = true; + ++count; } } - return ret; + if (count <= 0) + return false; + + Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option."); + return true; } public void DeleteDefaultValues() - => Changes = DeleteDefaultValues(this); + => Changes = DeleteDefaultValues(metaFileManager, imcChecker, this); public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.SetManipulations(container, this); + groupEditor.SetManipulations(container, this); Changes = false; } } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 43cfc1ee..3e367a3b 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -46,7 +46,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ if (!config.AutoReduplicateUiOnImport) return; - if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod) + if (modManager.Creator.LoadMod(modDirectory, false, false) is not { } mod) return; Dictionary> paths = []; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index d984d374..22cc0c86 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -79,7 +79,7 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd return false; } - modManager.AddMod(directory); + modManager.AddMod(directory, true); mod = modManager.LastOrDefault(); return mod != null && mod.ModPath == directory; } diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 59f8906e..bf1b6637 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -81,13 +81,13 @@ public sealed class ModManager : ModStorage, IDisposable, IService } /// Load a new mod and add it to the manager if successful. - public void AddMod(DirectoryInfo modFolder) + public void AddMod(DirectoryInfo modFolder, bool deleteDefaultMeta) { if (this.Any(m => m.ModPath.Name == modFolder.Name)) return; Creator.SplitMultiGroups(modFolder); - var mod = Creator.LoadMod(modFolder, true); + var mod = Creator.LoadMod(modFolder, true, deleteDefaultMeta); if (mod == null) return; @@ -141,7 +141,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService var oldName = mod.Name; _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!Creator.ReloadMod(mod, true, out var metaChange)) + if (!Creator.ReloadMod(mod, true, false, out var metaChange)) { Penumbra.Log.Warning(mod.Name.Length == 0 ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." @@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService dir.Refresh(); mod.ModPath = dir; - if (!Creator.ReloadMod(mod, false, out var metaChange)) + if (!Creator.ReloadMod(mod, false, false, out var metaChange)) { Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); return; @@ -332,7 +332,7 @@ public sealed class ModManager : ModStorage, IDisposable, IService var queue = new ConcurrentQueue(); Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => { - var mod = Creator.LoadMod(dir, false); + var mod = Creator.LoadMod(dir, false, false); if (mod != null) queue.Enqueue(mod); }); diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index c7eb7cc5..3e58c515 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -82,7 +82,7 @@ public static partial class ModMigration foreach (var (gamePath, swapPath) in swaps) mod.Default.FileSwaps.Add(gamePath, swapPath); - creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); + creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true, true); foreach (var group in mod.Groups) saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); @@ -182,7 +182,7 @@ public static partial class ModMigration Description = option.OptionDesc, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); return subMod; } @@ -196,7 +196,7 @@ public static partial class ModMigration Priority = priority, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); return subMod; } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 712630c6..7f18852d 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -39,7 +39,7 @@ public class ModGroupEditor( ImcModGroupEditor imcEditor, CommunicatorService communicator, SaveService saveService, - Configuration Config) : IService + Configuration config) : IService { public SingleModGroupEditor SingleEditor => singleEditor; @@ -57,7 +57,7 @@ public class ModGroupEditor( return; group.DefaultSettings = defaultOption; - saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); } @@ -68,9 +68,9 @@ public class ModGroupEditor( if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) return; - saveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateDelete(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); group.Name = newName; - saveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); } @@ -81,7 +81,7 @@ public class ModGroupEditor( var idx = group.GetIndex(); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); mod.Groups.RemoveAt(idx); - saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); } @@ -93,7 +93,7 @@ public class ModGroupEditor( if (!mod.Groups.Move(idxFrom, groupIdxTo)) return; - saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); } @@ -104,7 +104,7 @@ public class ModGroupEditor( return; group.Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); } @@ -115,7 +115,7 @@ public class ModGroupEditor( return; group.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); } @@ -126,7 +126,7 @@ public class ModGroupEditor( return; option.Name = newName; - saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } @@ -137,7 +137,7 @@ public class ModGroupEditor( return; option.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(option.Group, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); } @@ -149,7 +149,7 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Manipulations.SetTo(manipulations); - saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } @@ -161,13 +161,13 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.Files.SetTo(replacements); - saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } /// Forces a file save of the given container's group. public void ForceSave(IModDataContainer subMod, SaveType saveType = SaveType.Queue) - => saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + => saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) @@ -176,7 +176,7 @@ public class ModGroupEditor( subMod.Files.AddFrom(additions); if (oldCount != subMod.Files.Count) { - saveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.QueueSave(new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } } @@ -189,7 +189,7 @@ public class ModGroupEditor( communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); subMod.FileSwaps.SetTo(swaps); - saveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + saveService.Save(saveType, new ModSaveGroup(subMod, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0f4972e3..8cfdc9a7 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Data; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Meta; +using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; @@ -20,11 +21,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; public partial class ModCreator( - SaveService _saveService, + SaveService saveService, Configuration config, - ModDataEditor _dataEditor, - MetaFileManager _metaFileManager, - GamePathParser _gamePathParser) : IService + ModDataEditor dataEditor, + MetaFileManager metaFileManager, + GamePathParser gamePathParser, + ImcChecker imcChecker) : IService { public readonly Configuration Config = config; @@ -34,7 +36,7 @@ public partial class ModCreator( try { var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); - _dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); + dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; } @@ -46,7 +48,7 @@ public partial class ModCreator( } /// Load a mod by its directory. - public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges) + public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges, bool deleteDefaultMetaChanges) { modPath.Refresh(); if (!modPath.Exists) @@ -56,7 +58,7 @@ public partial class ModCreator( } var mod = new Mod(modPath); - if (ReloadMod(mod, incorporateMetaChanges, out _)) + if (ReloadMod(mod, incorporateMetaChanges, deleteDefaultMetaChanges, out _)) return mod; // Can not be base path not existing because that is checked before. @@ -65,21 +67,29 @@ public partial class ModCreator( } /// Reload a mod from its mod path. - public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange) + public bool ReloadMod(Mod mod, bool incorporateMetaChanges, bool deleteDefaultMetaChanges, out ModDataChangeType modDataChange) { modDataChange = ModDataChangeType.Deletion; if (!Directory.Exists(mod.ModPath.FullName)) return false; - modDataChange = _dataEditor.LoadMeta(this, mod); + modDataChange = dataEditor.LoadMeta(this, mod); if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) return false; - _dataEditor.LoadLocalData(mod); + dataEditor.LoadLocalData(mod); LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) IncorporateAllMetaChanges(mod, true); + if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges) + { + foreach (var container in mod.AllDataContainers) + { + if (ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, container.Manipulations)) + saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); + } + } return true; } @@ -89,13 +99,13 @@ public partial class ModCreator( { mod.Groups.Clear(); var changes = false; - foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) + foreach (var file in saveService.FileNames.GetOptionGroupFiles(mod)) { var group = LoadModGroup(mod, file); if (group != null && mod.Groups.All(g => g.Name != group.Name)) { changes = changes - || _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true) + || saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name, true) != Path.Combine(file.DirectoryName!, ReplaceBadXivSymbols(file.Name, true)); mod.Groups.Add(group); } @@ -106,13 +116,13 @@ public partial class ModCreator( } if (changes) - _saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); + saveService.SaveAllOptionGroups(mod, true, Config.ReplaceNonAsciiOnImport); } /// Load the default option for a given mod. public void LoadDefaultOption(Mod mod) { - var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); + var defaultFile = saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); try { var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); @@ -157,7 +167,7 @@ public partial class ModCreator( List deleteList = new(); foreach (var subMod in mod.AllDataContainers) { - var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); + var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false, true); changes |= localChanges; if (delete) deleteList.AddRange(localDeleteList); @@ -168,8 +178,8 @@ public partial class ModCreator( if (!changes) return; - _saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); + saveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } @@ -177,7 +187,7 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -194,7 +204,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName), + var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName), Config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); @@ -207,7 +217,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var rgsp = TexToolsMeta.FromRgspFile(_metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), + var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), Config.KeepDefaultMetaChanges); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); @@ -223,7 +233,11 @@ public partial class ModCreator( } DeleteDeleteList(deleteList, delete); - return (oldSize < option.Manipulations.Count, deleteList); + var changes = oldSize < option.Manipulations.Count; + if (deleteDefault && !Config.KeepDefaultMetaChanges) + changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, option.Manipulations); + + return (changes, deleteList); } /// @@ -250,7 +264,7 @@ public partial class ModCreator( group.Priority = priority; group.DefaultSettings = defaultSettings; group.OptionData.AddRange(subMods.Select(s => s.Clone(group))); - _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: @@ -260,7 +274,7 @@ public partial class ModCreator( group.Priority = priority; group.DefaultSettings = defaultSettings; group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group))); - _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } } @@ -277,7 +291,8 @@ public partial class ModCreator( foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); - IncorporateMetaChanges(mod, baseFolder, true); + IncorporateMetaChanges(mod, baseFolder, true, true); + return mod; } @@ -288,15 +303,15 @@ public partial class ModCreator( internal void CreateDefaultFiles(DirectoryInfo directory) { var mod = new Mod(directory); - ReloadMod(mod, false, out _); + ReloadMod(mod, false, false, out _); foreach (var file in mod.FindUnusedFiles()) { if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath)) mod.Default.Files.TryAdd(gamePath, file); } - IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); + IncorporateMetaChanges(mod.Default, directory, true, true); + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } /// Return the name of a new valid directory based on the base directory and the given name. @@ -333,7 +348,7 @@ public partial class ModCreator( { var mod = new Mod(baseDir); - var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList(); + var files = saveService.FileNames.GetOptionGroupFiles(mod).ToList(); var idx = 0; var reorder = false; foreach (var groupFile in files) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index e4049482..b5499624 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -97,7 +97,7 @@ public class TemporaryMod : IMod defaultMod.Manipulations.UnionWith(manips); saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); - modManager.AddMod(dir); + modManager.AddMod(dir, false); Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index b75c5aef..6ed1b55d 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -281,7 +281,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (newDir == null) return; - _modManager.AddMod(newDir); + _modManager.AddMod(newDir, false); var mod = _modManager[^1]; if (!_swapData.WriteMod(_modManager, mod, mod.Default, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2f76340b..8bdd95ab 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -180,7 +180,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Thu, 29 Aug 2024 18:38:09 +0200 Subject: [PATCH 381/865] Stop raising errors when compressing the deleted files after updating Heliosphere mods. --- OtterGui | 2 +- Penumbra/Api/Api/ModsApi.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index bfbde4f8..17bd4b75 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bfbde4f8aa6acc8eb3ed8bc419d5ae2afc77b5f1 +Subproject commit 17bd4b75b6d7750c92b65caf09715886d4df57cf diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 31f20c5e..64e201be 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -91,7 +91,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable if (_config.UseFileSystemCompression) new FileCompactor(Penumbra.Log).StartMassCompact(dir.EnumerateFiles("*.*", SearchOption.AllDirectories), - CompressionAlgorithm.Xpress8K); + CompressionAlgorithm.Xpress8K, false); return ApiHelpers.Return(PenumbraApiEc.Success, args); } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 27c7f2ed..9d8ea21c 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -816,13 +816,13 @@ public class SettingsTab : ITab, IUiService if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero, "Try to compress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, true); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero, "Try to decompress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, true); if (_compactor.MassCompactRunning) { From 5c5e45114f25f9429d8757b6edf852ecc37173c9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 18:38:37 +0200 Subject: [PATCH 382/865] Make loading mods for advanced editing async. --- Penumbra/Mods/Editor/ModEditor.cs | 65 +++++++++---- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 102 +++++++++++++++----- 2 files changed, 124 insertions(+), 43 deletions(-) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index cacb7f88..19ca7022 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -25,6 +25,21 @@ public class ModEditor( public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; public readonly FileCompactor Compactor = compactor; + + public bool IsLoading + { + get + { + lock (_lock) + { + return _loadingMod is { IsCompleted: false }; + } + } + } + + private readonly object _lock = new(); + private Task? _loadingMod; + public Mod? Mod { get; private set; } public int GroupIdx { get; private set; } public int DataIdx { get; private set; } @@ -32,28 +47,42 @@ public class ModEditor( public IModGroup? Group { get; private set; } public IModDataContainer? Option { get; private set; } - public void LoadMod(Mod mod) - => LoadMod(mod, -1, 0); - - public void LoadMod(Mod mod, int groupIdx, int dataIdx) + public async Task LoadMod(Mod mod, int groupIdx, int dataIdx) { - Mod = mod; - LoadOption(groupIdx, dataIdx, true); - Files.UpdateAll(mod, Option!); - SwapEditor.Revert(Option!); - MetaEditor.Load(Mod!, Option!); - Duplicates.Clear(); - MdlMaterialEditor.ScanModels(Mod!); + await AppendTask(() => + { + Mod = mod; + LoadOption(groupIdx, dataIdx, true); + Files.UpdateAll(mod, Option!); + SwapEditor.Revert(Option!); + MetaEditor.Load(Mod!, Option!); + Duplicates.Clear(); + MdlMaterialEditor.ScanModels(Mod!); + }); } - public void LoadOption(int groupIdx, int dataIdx) + private Task AppendTask(Action run) { - LoadOption(groupIdx, dataIdx, true); - SwapEditor.Revert(Option!); - Files.UpdatePaths(Mod!, Option!); - MetaEditor.Load(Mod!, Option!); - FileEditor.Clear(); - Duplicates.Clear(); + lock (_lock) + { + if (_loadingMod == null || _loadingMod.IsCompleted) + return _loadingMod = Task.Run(run); + + return _loadingMod = _loadingMod.ContinueWith(_ => run()); + } + } + + public async Task LoadOption(int groupIdx, int dataIdx) + { + await AppendTask(() => + { + LoadOption(groupIdx, dataIdx, true); + SwapEditor.Revert(Option!); + Files.UpdatePaths(Mod!, Option!); + MetaEditor.Load(Mod!, Option!); + FileEditor.Clear(); + Duplicates.Clear(); + }); } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 7bb067d8..f2fe8b9e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -8,6 +8,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -51,34 +52,68 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; - public Mod? Mod { get; private set; } + public Mod? Mod { get; private set; } + + + public bool IsLoading + { + get + { + lock (_lock) + { + return _editor.IsLoading || _loadingMod is { IsCompleted: false }; + } + } + } + + private readonly object _lock = new(); + private Task? _loadingMod; + + + private void AppendTask(Action run) + { + lock (_lock) + { + if (_loadingMod == null || _loadingMod.IsCompleted) + _loadingMod = Task.Run(run); + else + _loadingMod = _loadingMod.ContinueWith(_ => run()); + } + } public void ChangeMod(Mod mod) { if (mod == Mod) return; - _editor.LoadMod(mod, -1, 0); - Mod = mod; - - SizeConstraints = new WindowSizeConstraints + WindowName = $"{mod.Name} (LOADING){WindowBaseLabel}"; + AppendTask(() => { - MinimumSize = new Vector2(1240, 600), - MaximumSize = 4000 * Vector2.One, - }; - _selectedFiles.Clear(); - _modelTab.Reset(); - _materialTab.Reset(); - _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); - UpdateModels(); - _forceTextureStartPath = true; + _editor.LoadMod(mod, -1, 0).Wait(); + Mod = mod; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(1240, 600), + MaximumSize = 4000 * Vector2.One, + }; + _selectedFiles.Clear(); + _modelTab.Reset(); + _materialTab.Reset(); + _shaderPackageTab.Reset(); + _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); + UpdateModels(); + _forceTextureStartPath = true; + }); } public void ChangeOption(IModDataContainer? subMod) { - var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); - _editor.LoadOption(groupIdx, dataIdx); + AppendTask(() => + { + var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, dataIdx).Wait(); + }); } public void UpdateModels() @@ -92,6 +127,9 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public override void PreDraw() { + if (IsLoading) + return; + using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow); var sb = new StringBuilder(256); @@ -144,13 +182,16 @@ public partial class ModEditWindow : Window, IDisposable, IUiService public override void OnClose() { - _left.Dispose(); - _right.Dispose(); - _materialTab.Reset(); - _modelTab.Reset(); - _shaderPackageTab.Reset(); _config.Ephemeral.AdvancedEditingOpen = false; _config.Ephemeral.Save(); + AppendTask(() => + { + _left.Dispose(); + _right.Dispose(); + _materialTab.Reset(); + _modelTab.Reset(); + _shaderPackageTab.Reset(); + }); } public override void Draw() @@ -163,6 +204,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _config.Ephemeral.Save(); } + if (IsLoading) + { + var radius = 100 * ImUtf8.GlobalScale; + var thickness = (int) (20 * ImUtf8.GlobalScale); + var offsetX = ImGui.GetContentRegionAvail().X / 2 - radius; + var offsetY = ImGui.GetContentRegionAvail().Y / 2 - radius; + ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(offsetX, offsetY)); + ImUtf8.Spinner("##spinner"u8, radius, thickness, ImGui.GetColorU32(ImGuiCol.Text)); + return; + } + using var tabBar = ImRaii.TabBar("##tabs"); if (!tabBar) return; @@ -405,14 +457,14 @@ public partial class ModEditWindow : Window, IDisposable, IUiService if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", _editor.Option is DefaultSubMod)) { - _editor.LoadOption(-1, 0); + _editor.LoadOption(-1, 0).Wait(); ret = true; } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) { - _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); + _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait(); ret = true; } @@ -430,7 +482,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) { var (groupIdx, dataIdx) = option.GetDataIndices(); - _editor.LoadOption(groupIdx, dataIdx); + _editor.LoadOption(groupIdx, dataIdx).Wait(); ret = true; } } From de3644e9e131baf5f4f953bc0d034a116c7da4d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 18:46:37 +0200 Subject: [PATCH 383/865] Make BC4 textures importable. --- Penumbra/Import/Textures/TexFileParser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index ae4a39c0..1bf282e5 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -177,6 +177,7 @@ public static class TexFileParser DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, + DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, @@ -202,6 +203,7 @@ public static class TexFileParser TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, From 2a7d2ef0d5cef009c60a701235a1786e56d191b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 18:58:30 +0200 Subject: [PATCH 384/865] Allow reading BC6. --- Penumbra/Import/Textures/TexFileParser.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 1bf282e5..0d817fa1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -177,8 +177,9 @@ public static class TexFileParser DXGIFormat.BC1UNorm => TexFile.TextureFormat.BC1, DXGIFormat.BC2UNorm => TexFile.TextureFormat.BC2, DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, - DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, + DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, + DXGIFormat.BC6HUF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, @@ -203,8 +204,9 @@ public static class TexFileParser TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, - (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, + (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HUF16, // TODO: upstream to Lumina TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, From 176001195ba16d69c4540d2dbed9607932337ee6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 29 Aug 2024 21:13:33 +0200 Subject: [PATCH 385/865] Improve mod filters. --- OtterGui | 2 +- Penumbra/Import/Textures/TexFileParser.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 23 +++++---- Penumbra/UI/ModsTab/ModFilter.cs | 49 ++++++++++---------- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/OtterGui b/OtterGui index 17bd4b75..3e6b0857 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 17bd4b75b6d7750c92b65caf09715886d4df57cf +Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97 diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 0d817fa1..979e4d3c 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -204,7 +204,7 @@ public static class TexFileParser TexFile.TextureFormat.BC1 => DXGIFormat.BC1UNorm, TexFile.TextureFormat.BC2 => DXGIFormat.BC2UNorm, TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, - (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina + (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HUF16, // TODO: upstream to Lumina TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 8bdd95ab..7a165feb 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -9,6 +9,8 @@ using OtterGui.Filesystem; using OtterGui.FileSystem.Selector; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -84,8 +86,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector filter switch - { - ModFilter.Enabled => "Enabled", - ModFilter.Disabled => "Disabled", - ModFilter.Favorite => "Favorite", - ModFilter.NotFavorite => "No Favorite", - ModFilter.NoConflict => "No Conflicts", - ModFilter.SolvedConflict => "Solved Conflicts", - ModFilter.UnsolvedConflict => "Unsolved Conflicts", - ModFilter.HasNoMetaManipulations => "No Meta Manipulations", - ModFilter.HasMetaManipulations => "Meta Manipulations", - ModFilter.HasNoFileSwaps => "No File Swaps", - ModFilter.HasFileSwaps => "File Swaps", - ModFilter.HasNoConfig => "No Configuration", - ModFilter.HasConfig => "Configuration", - ModFilter.HasNoFiles => "No Files", - ModFilter.HasFiles => "Files", - ModFilter.IsNew => "Newly Imported", - ModFilter.NotNew => "Not Newly Imported", - ModFilter.Inherited => "Inherited Configuration", - ModFilter.Uninherited => "Own Configuration", - ModFilter.Undefined => "Not Configured", - _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null), - }; + public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs = + [ + (ModFilter.Enabled, ModFilter.Disabled, "Enabled"), + (ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"), + (ModFilter.Favorite, ModFilter.NotFavorite, "Favorite"), + (ModFilter.HasConfig, ModFilter.HasNoConfig, "Has Options"), + (ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"), + (ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"), + (ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"), + ]; + + public static IReadOnlyList> Groups = + [ + [ + (ModFilter.NoConflict, "Has No Conflicts"), + (ModFilter.SolvedConflict, "Has Solved Conflicts"), + (ModFilter.UnsolvedConflict, "Has Unsolved Conflicts"), + ], + [ + (ModFilter.Undefined, "Not Configured"), + (ModFilter.Inherited, "Inherited Configuration"), + (ModFilter.Uninherited, "Own Configuration"), + ], + ]; } From ff3e5410aac9e23606317e179f6278e710cb11ee Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 29 Aug 2024 19:18:17 +0000 Subject: [PATCH 386/865] [CI] Updating repo.json for testing_1.2.1.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 5a274d73..6f9b8c69 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.1", + "TestingAssemblyVersion": "1.2.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From fb144d0b74ce1b263eb3e69625c37518e3725a1b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:20:19 +0200 Subject: [PATCH 387/865] Cleanup. --- Penumbra/Import/Textures/TexFileParser.cs | 4 ++-- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 979e4d3c..220095c1 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -179,7 +179,7 @@ public static class TexFileParser DXGIFormat.BC3UNorm => TexFile.TextureFormat.BC3, DXGIFormat.BC4UNorm => (TexFile.TextureFormat)0x6120, // TODO: upstream to Lumina DXGIFormat.BC5UNorm => TexFile.TextureFormat.BC5, - DXGIFormat.BC6HUF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina + DXGIFormat.BC6HSF16 => (TexFile.TextureFormat)0x6330, // TODO: upstream to Lumina DXGIFormat.BC7UNorm => TexFile.TextureFormat.BC7, DXGIFormat.R16G16B16A16Typeless => TexFile.TextureFormat.D16, DXGIFormat.R24G8Typeless => TexFile.TextureFormat.D24S8, @@ -206,7 +206,7 @@ public static class TexFileParser TexFile.TextureFormat.BC3 => DXGIFormat.BC3UNorm, (TexFile.TextureFormat)0x6120 => DXGIFormat.BC4UNorm, // TODO: upstream to Lumina TexFile.TextureFormat.BC5 => DXGIFormat.BC5UNorm, - (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HUF16, // TODO: upstream to Lumina + (TexFile.TextureFormat)0x6330 => DXGIFormat.BC6HSF16, // TODO: upstream to Lumina TexFile.TextureFormat.BC7 => DXGIFormat.BC7UNorm, TexFile.TextureFormat.D16 => DXGIFormat.R16G16B16A16Typeless, TexFile.TextureFormat.D24S8 => DXGIFormat.R24G8Typeless, diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 81a33db6..07a54391 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -156,7 +156,7 @@ public class ModMetaEditor( } } - if (count <= 0) + if (count == 0) return false; Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option."); From 04582ba00b8fedfb32a8ad7fbed0230ea89126f7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:20:31 +0200 Subject: [PATCH 388/865] Add CustomArmor to UI events. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index a38e9bcf..97e9f427 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit a38e9bcfb80c456102945bbb4c59f5621cae0442 +Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623 diff --git a/Penumbra.GameData b/Penumbra.GameData index c43c5cac..bb281fb0 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c43c5cac4cee092bf0aed8d46bab112b037ef8f2 +Subproject commit bb281fb01d88d6fd815a286f87049978ef05de59 From 75858a61b5092b1567e332417610ef36a4f7e122 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:20:44 +0200 Subject: [PATCH 389/865] Fix MetaManipulations not resetting count when clearing. --- 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 1093c6c5..70d4fd47 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -69,6 +69,7 @@ public class MetaDictionary public void Clear() { + Count = 0; _imc.Clear(); _eqp.Clear(); _eqdp.Clear(); From 6b858dc5ac9d9cd967601a3fdac91048c87bf7c5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 14:23:34 +0200 Subject: [PATCH 390/865] Hmpf. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index bb281fb0..66bc00dc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bb281fb01d88d6fd815a286f87049978ef05de59 +Subproject commit 66bc00dc8517204e58c6515af5aec0ba6d196716 From 1b17404876d9248c77649b7831eda57332f84f96 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 31 Aug 2024 20:52:01 +0200 Subject: [PATCH 391/865] Fix small issue with changed item tooltips. --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6f0b63ce..b6b19ef2 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -114,7 +114,7 @@ public class Penumbra : IDalamudPlugin var itemSheet = _services.GetService().GetExcelSheet()!; _communicatorService.ChangedItemHover.Subscribe(it => { - if (it is IdentifiedItem) + if (it is IdentifiedItem { Item.Id.IsItem: true }) ImGui.TextUnformatted("Left Click to create an item link in chat."); }, ChangedItemHover.Priority.Link); From 22cbecc6a459a3700b0d5f663847f098c69963aa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Sep 2024 22:48:15 +0200 Subject: [PATCH 392/865] Add Page to mod group data for TT interop. --- Penumbra/Mods/Groups/IModGroup.cs | 23 +++++++++++++++-------- Penumbra/Mods/Groups/ImcModGroup.cs | 1 + Penumbra/Mods/Groups/MultiModGroup.cs | 1 + Penumbra/Mods/Groups/SingleModGroup.cs | 1 + 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index c5654019..a6f6e20d 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -24,14 +24,21 @@ public interface IModGroup { public const int MaxMultiOptions = 32; - public Mod Mod { get; } - public string Name { get; set; } - public string Description { get; set; } - public string Image { get; set; } - public GroupType Type { get; } - public GroupDrawBehaviour Behaviour { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; set; } + public string Description { get; set; } + + /// Unused in Penumbra but for better TexTools interop. + public string Image { get; set; } + + public GroupType Type { get; } + public GroupDrawBehaviour Behaviour { get; } + public ModPriority Priority { get; set; } + + /// Unused in Penumbra but for better TexTools interop. + public int Page { get; set; } + + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); public IModOption? AddOption(string name, string description = ""); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index d42804ba..2b020184 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -29,6 +29,7 @@ public class ImcModGroup(Mod mod) : IModGroup => GroupDrawBehaviour.MultiSelection; public ModPriority Priority { get; set; } = ModPriority.Default; + public int Page { get; set; } public Setting DefaultSettings { get; set; } = Setting.Zero; public ImcIdentifier Identifier; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 9cf7e6a3..24dcc849 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -28,6 +28,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public string Description { get; set; } = string.Empty; public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } + public int Page { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 723cd5b1..fddb96d6 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -26,6 +26,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public string Description { get; set; } = string.Empty; public string Image { get; set; } = string.Empty; public ModPriority Priority { get; set; } + public int Page { get; set; } public Setting DefaultSettings { get; set; } public readonly List OptionData = []; From bd59591ed8650c5ae544fa3546fbc5e4fa5e813b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Sep 2024 23:42:19 +0200 Subject: [PATCH 393/865] Add display of ImportDate and allow resetting it, add button to open local data json. --- Penumbra/Mods/Manager/ModDataEditor.cs | 11 ++++++ Penumbra/Mods/ModCreator.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 19 +++++----- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 37 ++++++++++++++++++++ 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7a0467d0..933620d9 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -249,6 +249,17 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } + public void ResetModImportDate(Mod mod) + { + var newDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (mod.ImportDate == newDate) + return; + + mod.ImportDate = newDate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.ImportDate, mod, null); + } + public void ChangeModNote(Mod mod, string newNote) { if (mod.Note == newNote) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 8cfdc9a7..fe027ca4 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -77,7 +77,7 @@ public partial class ModCreator( if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) return false; - dataEditor.LoadLocalData(mod); + modDataChange |= dataEditor.LoadLocalData(mod); LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 7a165feb..0781312c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -447,16 +447,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Mon, 9 Sep 2024 14:10:54 +0200 Subject: [PATCH 394/865] Allow copying paths out of the resource logger. --- .../ResourceWatcher/ResourceWatcherTable.cs | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 33e301ae..2bb71b87 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Table; +using OtterGui.Text; using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; @@ -52,36 +53,41 @@ internal sealed class ResourceWatcherTable : Table private static unsafe void DrawByteString(CiByteString path, float length) { - Vector2 vec; - ImGuiNative.igCalcTextSize(&vec, path.Path, path.Path + path.Length, 0, 0); - if (vec.X <= length) + if (path.IsEmpty) + return; + + var size = ImUtf8.CalcTextSize(path.Span); + var clicked = false; + if (size.X <= length) { - ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); + clicked = ImUtf8.Selectable(path.Span); } else { - var fileName = path.LastIndexOf((byte)'/'); - CiByteString shortPath; - if (fileName != -1) + var fileName = path.LastIndexOf((byte)'/'); + using (ImRaii.Group()) { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * UiHelpers.Scale)); - using var font = ImRaii.PushFont(UiBuilder.IconFont); - ImGui.TextUnformatted(FontAwesomeIcon.EllipsisH.ToIconString()); - ImGui.SameLine(); - shortPath = path.Substring(fileName, path.Length - fileName); - } - else - { - shortPath = path; + CiByteString shortPath; + if (fileName != -1) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + clicked = ImUtf8.Selectable(FontAwesomeIcon.EllipsisH.ToIconString()); + ImUtf8.SameLineInner(); + shortPath = path.Substring(fileName, path.Length - fileName); + } + else + { + shortPath = path; + } + + clicked |= ImUtf8.Selectable(shortPath.Span, false, ImGuiSelectableFlags.AllowItemOverlap); } - ImGuiNative.igTextUnformatted(shortPath.Path, shortPath.Path + shortPath.Length); - if (ImGui.IsItemClicked()) - ImGuiNative.igSetClipboardText(path.Path); - - if (ImGui.IsItemHovered()) - ImGuiNative.igSetTooltip(path.Path); + ImUtf8.HoverTooltip(path.Span); } + + if (clicked) + ImUtf8.SetClipboardText(path.Span); } private sealed class RecordTypeColumn : ColumnFlags From 10ce5da8c9bb5d2b226a940a23f1ef57026aec81 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 9 Sep 2024 13:42:42 +0000 Subject: [PATCH 395/865] [CI] Updating repo.json for testing_1.2.1.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6f9b8c69..cdd622c9 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.2", + "TestingAssemblyVersion": "1.2.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 26371d42f75768a0bda28da0f2fc8976075fdb82 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Sep 2024 16:51:23 +0200 Subject: [PATCH 396/865] Be less dumb. --- Penumbra/Mods/Groups/ImcModGroup.cs | 7 +------ Penumbra/Mods/Groups/ModSaveGroup.cs | 17 +++++++++++++++++ Penumbra/Mods/Groups/MultiModGroup.cs | 13 ++++--------- Penumbra/Mods/Groups/SingleModGroup.cs | 13 ++++--------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 2b020184..f8b4b2ef 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -170,14 +170,10 @@ public class ImcModGroup(Mod mod) : IModGroup var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); var ret = new ImcModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, }; - if (ret.Name.Length == 0) + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; if (!identifier.HasValue || ret.DefaultEntry.MaterialId == 0) @@ -216,7 +212,6 @@ public class ImcModGroup(Mod mod) : IModGroup } ret.Identifier = identifier.Value; - ret.DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero; ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); return ret; } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index c465822b..bda70b54 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -1,4 +1,7 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; @@ -90,6 +93,8 @@ public readonly struct ModSaveGroup : ISavable jWriter.WriteValue(group.Description); jWriter.WritePropertyName(nameof(group.Image)); jWriter.WriteValue(group.Image); + jWriter.WritePropertyName(nameof(group.Page)); + jWriter.WriteValue(group.Page); jWriter.WritePropertyName(nameof(group.Priority)); jWriter.WriteValue(group.Priority.Value); jWriter.WritePropertyName(nameof(group.Type)); @@ -97,4 +102,16 @@ public readonly struct ModSaveGroup : ISavable jWriter.WritePropertyName(nameof(group.DefaultSettings)); jWriter.WriteValue(group.DefaultSettings.Value); } + + public static bool ReadJsonBase(JObject json, IModGroup group) + { + group.Name = json[nameof(IModGroup.Name)]?.ToObject() ?? string.Empty; + group.Description = json[nameof(IModGroup.Description)]?.ToObject() ?? string.Empty; + group.Image = json[nameof(IModGroup.Image)]?.ToObject() ?? string.Empty; + group.Page = json[nameof(IModGroup.Page)]?.ToObject() ?? 0; + group.Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + group.DefaultSettings = json[nameof(IModGroup.DefaultSettings)]?.ToObject() ?? Setting.Zero; + + return group.Name.Length > 0; + } } diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 24dcc849..0c9aa805 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -67,15 +67,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public static MultiModGroup? Load(Mod mod, JObject json) { - var ret = new MultiModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - }; - if (ret.Name.Length == 0) + var ret = new MultiModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; var options = json["Options"]; @@ -106,6 +99,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup Name = Name, Description = Description, Priority = Priority, + Image = Image, + Page = Page, DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single))); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index fddb96d6..ab0c2d4f 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -63,15 +63,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public static SingleModGroup? Load(Mod mod, JObject json) { var options = json["Options"]; - var ret = new SingleModGroup(mod) - { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Image = json[nameof(Image)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, - }; - if (ret.Name.Length == 0) + var ret = new SingleModGroup(mod); + if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; if (options != null) @@ -92,6 +85,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup Name = Name, Description = Description, Priority = Priority, + Image = Image, + Page = Page, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i)))); From ac1ea124d93e9a17e6fe0fe71b920d31d4c5fe99 Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 9 Sep 2024 14:53:17 +0000 Subject: [PATCH 397/865] [CI] Updating repo.json for testing_1.2.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cdd622c9..6625cb24 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.3", + "TestingAssemblyVersion": "1.2.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 00fbb2686b864dcfe91bb9e67415b9059fc53a55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Sep 2024 22:54:06 +0200 Subject: [PATCH 398/865] Add option to apply only attributes from IMC group. --- Penumbra/Meta/ImcChecker.cs | 25 +++++++--------- Penumbra/Mods/Editor/ModMetaEditor.cs | 9 +++--- Penumbra/Mods/Groups/ImcModGroup.cs | 30 +++++++++++++------ .../Manager/OptionEditor/ImcModGroupEditor.cs | 10 +++++++ Penumbra/Mods/ModCreator.cs | 7 ++--- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 4 +-- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 6 ++-- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 12 ++++++-- 8 files changed, 63 insertions(+), 40 deletions(-) diff --git a/Penumbra/Meta/ImcChecker.cs b/Penumbra/Meta/ImcChecker.cs index 4e3ff11b..a415c9b0 100644 --- a/Penumbra/Meta/ImcChecker.cs +++ b/Penumbra/Meta/ImcChecker.cs @@ -6,9 +6,9 @@ namespace Penumbra.Meta; public class ImcChecker { - private static readonly Dictionary VariantCounts = []; - private static MetaFileManager? _dataManager; - + private static readonly Dictionary VariantCounts = []; + private static MetaFileManager? _dataManager; + private static readonly ConcurrentDictionary GlobalCachedDefaultEntries = []; public static int GetVariantCount(ImcIdentifier identifier) { @@ -26,23 +26,20 @@ public class ImcChecker public readonly record struct CachedEntry(ImcEntry Entry, bool FileExists, bool VariantExists); - private readonly Dictionary _cachedDefaultEntries = new(); - private readonly MetaFileManager _metaFileManager; - public ImcChecker(MetaFileManager metaFileManager) - { - _metaFileManager = metaFileManager; - _dataManager = metaFileManager; - } + => _dataManager = metaFileManager; - public CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) + public static CachedEntry GetDefaultEntry(ImcIdentifier identifier, bool storeCache) { - if (_cachedDefaultEntries.TryGetValue(identifier, out var entry)) + if (GlobalCachedDefaultEntries.TryGetValue(identifier, out var entry)) return entry; + if (_dataManager == null) + return new CachedEntry(default, false, false); + try { - var e = ImcFile.GetDefault(_metaFileManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); + var e = ImcFile.GetDefault(_dataManager, identifier.GamePath(), identifier.EquipSlot, identifier.Variant, out var entryExists); entry = new CachedEntry(e, true, entryExists); } catch (Exception) @@ -51,7 +48,7 @@ public class ImcChecker } if (storeCache) - _cachedDefaultEntries.Add(identifier, entry); + GlobalCachedDefaultEntries.TryAdd(identifier, entry); return entry; } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 07a54391..64c585ea 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -10,8 +10,7 @@ namespace Penumbra.Mods.Editor; public class ModMetaEditor( ModGroupEditor groupEditor, - MetaFileManager metaFileManager, - ImcChecker imcChecker) : MetaDictionary, IService + MetaFileManager metaFileManager) : MetaDictionary, IService { public sealed class OtherOptionData : HashSet { @@ -67,14 +66,14 @@ public class ModMetaEditor( Changes = false; } - public static bool DeleteDefaultValues(MetaFileManager metaFileManager, ImcChecker imcChecker, MetaDictionary dict) + public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { var clone = dict.Clone(); dict.Clear(); var count = 0; foreach (var (key, value) in clone.Imc) { - var defaultEntry = imcChecker.GetDefaultEntry(key, false); + var defaultEntry = ImcChecker.GetDefaultEntry(key, false); if (!defaultEntry.Entry.Equals(value)) { dict.TryAdd(key, value); @@ -164,7 +163,7 @@ public class ModMetaEditor( } public void DeleteDefaultValues() - => Changes = DeleteDefaultValues(metaFileManager, imcChecker, this); + => Changes = DeleteDefaultValues(metaFileManager, this); public void Apply(IModDataContainer container) { diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index f8b4b2ef..2a1854ed 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -35,6 +35,7 @@ public class ImcModGroup(Mod mod) : IModGroup public ImcIdentifier Identifier; public ImcEntry DefaultEntry; public bool AllVariants; + public bool OnlyAttributes; public FullPath? FindBestMatch(Utf8GamePath gamePath) @@ -97,28 +98,36 @@ public class ImcModGroup(Mod mod) : IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) => new ImcModGroupEditDrawer(editDrawer, this); - public ImcEntry GetEntry(ushort mask) - => DefaultEntry with { AttributeMask = mask }; + private ImcEntry GetEntry(Variant variant, ushort mask) + { + if (!OnlyAttributes) + return DefaultEntry with { AttributeMask = mask }; + + var defaultEntry = ImcChecker.GetDefaultEntry(Identifier with { Variant = variant }, true); + if (defaultEntry.VariantExists) + return defaultEntry.Entry with { AttributeMask = mask }; + + return DefaultEntry with { AttributeMask = mask }; + } public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) { if (IsDisabled(setting)) return; - var mask = GetCurrentMask(setting); - var entry = GetEntry(mask); + var mask = GetCurrentMask(setting); if (AllVariants) { var count = ImcChecker.GetVariantCount(Identifier); if (count == 0) - manipulations.TryAdd(Identifier, entry); + manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask)); else for (var i = 0; i <= count; ++i) - manipulations.TryAdd(Identifier with { Variant = (Variant)i }, entry); + manipulations.TryAdd(Identifier with { Variant = (Variant)i }, GetEntry((Variant)i, mask)); } else { - manipulations.TryAdd(Identifier, entry); + manipulations.TryAdd(Identifier, GetEntry(Identifier.Variant, mask)); } } @@ -138,6 +147,8 @@ public class ImcModGroup(Mod mod) : IModGroup serializer.Serialize(jWriter, DefaultEntry); jWriter.WritePropertyName(nameof(AllVariants)); jWriter.WriteValue(AllVariants); + jWriter.WritePropertyName(nameof(OnlyAttributes)); + jWriter.WriteValue(OnlyAttributes); jWriter.WritePropertyName("Options"); jWriter.WriteStartArray(); foreach (var option in OptionData) @@ -170,8 +181,9 @@ public class ImcModGroup(Mod mod) : IModGroup var identifier = ImcIdentifier.FromJson(json[nameof(Identifier)] as JObject); var ret = new ImcModGroup(mod) { - DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), - AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, + DefaultEntry = json[nameof(DefaultEntry)]?.ToObject() ?? new ImcEntry(), + AllVariants = json[nameof(AllVariants)]?.ToObject() ?? false, + OnlyAttributes = json[nameof(OnlyAttributes)]?.ToObject() ?? false, }; if (!ModSaveGroup.ReadJsonBase(json, ret)) return null; diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index 515f6ff4..dc94c881 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -89,6 +89,16 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); } + public void ChangeOnlyAttributes(ImcModGroup group, bool onlyAttributes, SaveType saveType = SaveType.Queue) + { + if (group.OnlyAttributes == onlyAttributes) + return; + + group.OnlyAttributes = onlyAttributes; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) { if (group.CanBeDisabled == canBeDisabled) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index fe027ca4..1af9c1db 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -25,8 +25,7 @@ public partial class ModCreator( Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager, - GamePathParser gamePathParser, - ImcChecker imcChecker) : IService + GamePathParser gamePathParser) : IService { public readonly Configuration Config = config; @@ -86,7 +85,7 @@ public partial class ModCreator( { foreach (var container in mod.AllDataContainers) { - if (ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, container.Manipulations)) + if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations)) saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); } } @@ -235,7 +234,7 @@ public partial class ModCreator( DeleteDeleteList(deleteList, delete); var changes = oldSize < option.Manipulations.Count; if (deleteDefault && !Config.KeepDefaultMetaChanges) - changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, imcChecker, option.Manipulations); + changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, option.Manipulations); return (changes, deleteList); } diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index 53c61292..c8310cf7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -30,7 +30,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } private void UpdateEntry() - => (Entry, _fileExists, _) = MetaFiles.ImcChecker.GetDefaultEntry(Identifier, true); + => (Entry, _fileExists, _) = ImcChecker.GetDefaultEntry(Identifier, true); protected override void DrawNew() { @@ -54,7 +54,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile DrawMetaButtons(identifier, entry); DrawIdentifier(identifier); - var defaultEntry = MetaFiles.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; if (DrawEntry(defaultEntry, ref entry, true)) Editor.Changes |= Editor.Update(identifier, entry); } diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index 689571f3..c30239bc 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -24,13 +24,11 @@ public class AddGroupDrawer : IUiService private bool _imcFileExists; private bool _entryExists; private bool _entryInvalid; - private readonly ImcChecker _imcChecker; private readonly ModManager _modManager; - public AddGroupDrawer(ModManager modManager, ImcChecker imcChecker) + public AddGroupDrawer(ModManager modManager) { _modManager = modManager; - _imcChecker = imcChecker; UpdateEntry(); } @@ -142,7 +140,7 @@ public class AddGroupDrawer : IUiService private void UpdateEntry() { - (_defaultEntry, _imcFileExists, _entryExists) = _imcChecker.GetDefaultEntry(_imcIdentifier, false); + (_defaultEntry, _imcFileExists, _entryExists) = ImcChecker.GetDefaultEntry(_imcIdentifier, false); _entryInvalid = !_imcIdentifier.Validate() || _defaultEntry.MaterialId == 0 || !_entryExists; } } diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 4ab1c6aa..786bb8ff 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -6,6 +6,7 @@ using OtterGui.Text; using OtterGui.Text.Widget; using OtterGuiInternal.Utility; using Penumbra.GameData.Structs; +using Penumbra.Meta; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; @@ -18,18 +19,25 @@ public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGr public void Draw() { var identifier = group.Identifier; - var defaultEntry = editor.ImcChecker.GetDefaultEntry(identifier, true).Entry; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; var entry = group.DefaultEntry; var changes = false; - var width = editor.AvailableWidth.X - ImUtf8.ItemInnerSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X; + var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - ImUtf8.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight; ImUtf8.TextFramed(identifier.ToString(), 0, new Vector2(width, 0), borderColor: ImGui.GetColorU32(ImGuiCol.Border)); + ImUtf8.SameLineInner(); var allVariants = group.AllVariants; if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); ImUtf8.HoverTooltip("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); + ImGui.SameLine(); + var onlyAttributes = group.OnlyAttributes; + if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes)) + editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes); + ImUtf8.HoverTooltip("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8); + using (ImUtf8.Group()) { ImUtf8.TextFrameAligned("Material ID"u8); From 9b958a9d37856e060f66643ad306de3ea63b0bf2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Sep 2024 23:16:43 +0200 Subject: [PATCH 399/865] Update actions. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b40b2538..1783c9a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Release/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c9e2909..4799cbed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Release/* diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 91361646..0718ded2 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -37,7 +37,7 @@ jobs: - name: Archive run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip - name: Upload a Build Artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v4 with: path: | ./Penumbra/bin/Debug/* From af2a14826cdee2472b9ac872e1808d505aae14e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 19 Sep 2024 22:50:00 +0200 Subject: [PATCH 400/865] Add potential hidden priorities. --- Penumbra/Mods/Settings/ModPriority.cs | 6 ++++++ Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 5 +++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Penumbra/Mods/Settings/ModPriority.cs b/Penumbra/Mods/Settings/ModPriority.cs index 993bd577..cf234c00 100644 --- a/Penumbra/Mods/Settings/ModPriority.cs +++ b/Penumbra/Mods/Settings/ModPriority.cs @@ -66,4 +66,10 @@ public readonly record struct ModPriority(int Value) : public int CompareTo(ModPriority other) => Value.CompareTo(other.Value); + + public const int HiddenMin = -84037; + public const int HiddenMax = HiddenMin + 1000; + + public bool IsHidden + => Value is > HiddenMin and < HiddenMax; } diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index bee48068..bc18ac51 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -25,7 +25,7 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy => "Conflicts"u8; public bool IsVisible - => collectionManager.Active.Current.Conflicts(selector.Selected!).Count > 0; + => collectionManager.Active.Current.Conflicts(selector.Selected!).Any(c => !GetPriority(c).IsHidden); private readonly ConditionalWeakTable _expandedMods = []; @@ -58,7 +58,8 @@ public class ModPanelConflictsTab(CollectionManager collectionManager, ModFileSy // Can not be null because otherwise the tab bar is never drawn. var mod = selector.Selected!; - foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).OrderByDescending(GetPriority) + foreach (var (conflict, index) in collectionManager.Active.Current.Conflicts(mod).Where(c => !c.Mod2.Priority.IsHidden) + .OrderByDescending(GetPriority) .ThenBy(c => c.Mod2.Name.Lower).WithIndex()) { using var id = ImRaii.PushId(index); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index d2fbd0cd..8d889c3b 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.UI.Classes; using Penumbra.Collections.Manager; @@ -96,6 +97,9 @@ public class ModPanelSettingsTab( ImGui.SetNextItemWidth(50 * UiHelpers.Scale); if (ImGui.InputInt("##Priority", ref priority, 0, 0)) _currentPriority = priority; + if (new ModPriority(priority).IsHidden) + ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { From 22aca49112b7f9d6feca07c999507a08ff41935c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 22 Sep 2024 19:47:34 +0000 Subject: [PATCH 401/865] [CI] Updating repo.json for testing_1.2.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 6625cb24..36b27682 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.4", + "TestingAssemblyVersion": "1.2.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From caf4382e1f17b8fccd1bfc2f03293fd149596971 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 11:50:49 +0200 Subject: [PATCH 402/865] Update BNPCs --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 66bc00dc..fd50cb3d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 66bc00dc8517204e58c6515af5aec0ba6d196716 +Subproject commit fd50cb3d33e8f59e8b60474c3def914a6952c485 From 776b4e9efbd1835cc0332d7f1ea9b68c4267bf72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 11:58:00 +0200 Subject: [PATCH 403/865] Update obsolete properties from CS. --- Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs | 4 ++-- .../UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index 61ccc95c..c459a67a 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -40,8 +40,8 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase if (_originalColorTableTexture.Texture == null) throw new InvalidOperationException("Material doesn't have a color table"); - Width = (int)_originalColorTableTexture.Texture->Width; - Height = (int)_originalColorTableTexture.Texture->Height; + Width = (int)_originalColorTableTexture.Texture->ActualWidth; + Height = (int)_originalColorTableTexture.Texture->ActualHeight; ColorTable = new Half[Width * Height * 4]; _updatePending = true; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs index 6ffd1f88..5c636b1d 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MaterialTemplatePickers.cs @@ -56,7 +56,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService } var firstNonNullTexture = firstNonNullTextureRH != null ? firstNonNullTextureRH->CsHandle.Texture : null; - var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->Width, firstNonNullTexture->Height).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; + var textureSize = firstNonNullTexture != null ? new Vector2(firstNonNullTexture->ActualWidth, firstNonNullTexture->ActualHeight).Contain(new Vector2(MaximumTextureSize)) : Vector2.Zero; var count = firstNonNullTexture != null ? firstNonNullTexture->ArraySize : 0; var ret = false; @@ -135,10 +135,10 @@ public sealed unsafe class MaterialTemplatePickers : IUiService continue; var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j }; - var size = new Vector2(texture->Width, texture->Height).Contain(itemSize); + var size = new Vector2(texture->ActualWidth, texture->ActualHeight).Contain(itemSize); position += (itemSize - size) * 0.5f; ImGui.GetWindowDrawList().AddImage(handle, position, position + size, Vector2.Zero, - new Vector2(texture->Width / (float)texture->Width2, texture->Height / (float)texture->Height2)); + new Vector2(texture->ActualWidth / (float)texture->AllocatedWidth, texture->ActualHeight / (float)texture->AllocatedHeight)); } } From 389c42e68f2d65eaa7ec8eff6a1fd38c955faf54 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 13:02:28 +0200 Subject: [PATCH 404/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fd50cb3d..dd86dafb 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fd50cb3d33e8f59e8b60474c3def914a6952c485 +Subproject commit dd86dafb88ca4c7b662938bbc1310729ba7f788d From 8084f481446dd601761c0c96bf6baf1d1366fb63 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:29:39 +1000 Subject: [PATCH 405/865] Init support for DT model i/o --- Penumbra/Import/Models/Export/MeshExporter.cs | 62 +++- Penumbra/Import/Models/Import/MeshImporter.cs | 32 ++- .../Import/Models/Import/VertexAttribute.cs | 265 ++++++++++++++---- 3 files changed, 281 insertions(+), 78 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 3a57ab55..20158776 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -311,15 +311,28 @@ public class MeshExporter MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), MdlFile.VertexType.UByte4 => reader.ReadBytes(4), - MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, - reader.ReadByte() / 255f), + MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), - (float)reader.ReadHalf()), - + MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), + MdlFile.VertexType.UShort4 => ReadUShort4(reader), var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } + + private byte[] ReadUShort4(BinaryReader reader) + { + var buffer = reader.ReadBytes(8); + var byteValues = new byte[8]; + byteValues[0] = buffer[0]; + byteValues[4] = buffer[1]; + byteValues[1] = buffer[2]; + byteValues[5] = buffer[3]; + byteValues[2] = buffer[4]; + byteValues[6] = buffer[5]; + byteValues[3] = buffer[6]; + byteValues[7] = buffer[7]; + return byteValues; + } /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlyDictionary usages) @@ -444,7 +457,16 @@ public class MeshExporter private static Type GetSkinningType(IReadOnlyDictionary usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) - return typeof(VertexJoints4); + { + if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + { + return typeof(VertexJoints8); + } + else + { + return typeof(VertexJoints4); + } + } return typeof(VertexEmpty); } @@ -455,15 +477,17 @@ public class MeshExporter if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); - if (_skinningType == typeof(VertexJoints4)) + if (_skinningType == typeof(VertexJoints4) || _skinningType == typeof(VertexJoints8)) { if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indices = ToByteArray(attributes[MdlFile.VertexUsage.BlendIndices]); - var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]); - - var bindings = Enumerable.Range(0, 4) + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indices = ToByteArray(indiciesData); + var weights = ToFloatArray(weightsData); + + var bindings = Enumerable.Range(0, indices.Length) .Select(bindingIndex => { // NOTE: I've not seen any files that throw this error that aren't completely broken. @@ -474,7 +498,13 @@ public class MeshExporter return (jointIndex, weights[bindingIndex]); }) .ToArray(); - return new VertexJoints4(bindings); + + return bindings.Length switch + { + 4 => new VertexJoints4(bindings), + 8 => new VertexJoints8(bindings), + _ => throw _notifier.Exception($"Invalid number of bone bindings {bindings.Length}.") + }; } throw _notifier.Exception($"Unknown skinning type {_skinningType}"); @@ -517,4 +547,12 @@ public class MeshExporter byte[] value => value, _ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"), }; + + private static float[] ToFloatArray(object data) + => data switch + { + byte[] value => value.Select(x => x / 255f).ToArray(), + _ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"), + }; } + diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 1df97907..e3567780 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -194,17 +194,37 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) foreach (var (primitive, primitiveIndex) in node.Mesh.Primitives.WithIndex()) { // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. - var jointsAccessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); - var weightsAccessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); + var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); + var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); - if (jointsAccessor == null || weightsAccessor == null) + if (joints0Accessor == null || weights0Accessor == null) throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); // Build a set of joints that are referenced by this mesh. - for (var i = 0; i < jointsAccessor.Count; i++) + for (var i = 0; i < joints0Accessor.Count; i++) { - var joints = jointsAccessor[i]; - var weights = weightsAccessor[i]; + var joints = joints0Accessor[i]; + var weights = weights0Accessor[i]; + for (var index = 0; index < 4; index++) + { + // If a joint has absolutely no weight, we omit the bone entirely. + if (weights[index] == 0) + continue; + + usedJoints.Add((ushort)joints[index]); + } + } + + var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); + var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); + + if (joints1Accessor == null || weights1Accessor == null) + continue; + + for (var i = 0; i < joints1Accessor.Count; i++) + { + var joints = joints1Accessor[i]; + var weights = weights1Accessor[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index af401ec1..b71ad429 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -40,6 +40,7 @@ public class VertexAttribute MdlFile.VertexType.NByte4 => 4, MdlFile.VertexType.Half2 => 4, MdlFile.VertexType.Half4 => 8, + MdlFile.VertexType.UShort4 => 8, _ => throw new Exception($"Unhandled vertex type {(MdlFile.VertexType)Element.Type}"), }; @@ -121,89 +122,219 @@ public class VertexAttribute public static VertexAttribute? BlendWeight(Accessors accessors, IoNotifier notifier) { - if (!accessors.TryGetValue("WEIGHTS_0", out var accessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) return null; if (!accessors.ContainsKey("JOINTS_0")) throw notifier.Exception("Mesh contained WEIGHTS_0 attribute but no corresponding JOINTS_0 attribute."); - var element = new MdlStructs.VertexElement() + if (accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; + if (!accessors.ContainsKey("JOINTS_1")) + throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute."); + + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; - var values = accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + var weights1 = weights1Accessor.AsVector4Array(); - return new VertexAttribute( - element, - index => { - // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off - // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak - // the converted values to have the expected sum, preferencing values with minimal differences. - var originalValues = values[index]; - var byteValues = BuildNByte4(originalValues); - - var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); - while (adjustment != 0) - { - var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); - var closestIndex = Enumerable.Range(0, 4) - .Where(index => { - var byteValue = byteValues[index]; - if (adjustment < 0) return byteValue > 0; - if (adjustment > 0) return byteValue < 255; - return true; - }) - .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) - .MinBy(x => x.delta) - .index; - byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); - adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = weights1[index]; + var originalData = BuildUshort4(weight0, weight1); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); } - - return byteValues; - } - ); + ); + } + else + { + var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = Vector4.Zero; + var originalData = BuildUshort4(weight0, weight1); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); + } + ); + + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.NByte4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => + { + var weight0 = weights0[index]; + var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + var newByteValues = AdjustByteArray(byteValues, originalData); + if (!newByteValues.SequenceEqual(byteValues)) + notifier.Warning("Adjusted blend weights to maintain precision."); + return newByteValues; + });*/ + } + } + + private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) + { + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off + // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak + // the converted values to have the expected sum, preferencing values with minimal differences. + var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + while (adjustment != 0) + { + var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); + var closestIndex = Enumerable.Range(0, byteValues.Length) + .Where(index => + { + var byteValue = byteValues[index]; + if (adjustment < 0) + return byteValue > 0; + if (adjustment > 0) + return byteValue < 255; + + return true; + }) + .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .MinBy(x => x.delta) + .index; + byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); + adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + } + + return byteValues; } public static VertexAttribute? BlendIndex(Accessors accessors, IDictionary? boneMap, IoNotifier notifier) { - if (!accessors.TryGetValue("JOINTS_0", out var jointsAccessor)) + if (!accessors.TryGetValue("JOINTS_0", out var joints0Accessor)) return null; - if (!accessors.TryGetValue("WEIGHTS_0", out var weightsAccessor)) + if (!accessors.TryGetValue("WEIGHTS_0", out var weights0Accessor)) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no corresponding WEIGHTS_0 attribute."); if (boneMap == null) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); - var element = new MdlStructs.VertexElement() + var joints0 = joints0Accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + + if (accessors.TryGetValue("JOINTS_1", out var joints1Accessor)) { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; + if (!accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) + throw notifier.Exception("Mesh contained JOINTS_1 attribute but no corresponding WEIGHTS_1 attribute."); - var joints = jointsAccessor.AsVector4Array(); - var weights = weightsAccessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => + var element = new MdlStructs.VertexElement { - var gltfIndices = joints[index]; - var gltfWeights = weights[index]; + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - ); + var joints1 = joints1Accessor.AsVector4Array(); + var weights1 = weights1Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => + { + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var gltfIndices1 = joints1[index]; + var gltfWeights1 = weights1[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + var v1 = new Vector4( + gltfWeights1.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.X], + gltfWeights1.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Y], + gltfWeights1.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Z], + gltfWeights1.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.W] + ); + + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + } + else + { + var element = new MdlStructs.VertexElement + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + return new VertexAttribute( + element, + index => + { + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + var v1 = Vector4.Zero; + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.UByte4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; + + return new VertexAttribute( + element, + index => + { + var gltfIndices = joints0[index]; + var gltfWeights = weights0[index]; + return BuildUByte4(new Vector4( + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] + )); + } + );*/ + } } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) @@ -232,7 +363,7 @@ public class VertexAttribute var value = values[vertexIndex]; var delta = morphValues[morphIndex]?[vertexIndex]; - if (delta != null) + if (delta != null) value += delta.Value; return BuildSingle3(value); @@ -489,4 +620,18 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; + + private static float[] BuildUshort4(Vector4 v0, Vector4 v1) + { + var buf = new float[8]; + buf[0] = v0.X; + buf[4] = v1.X; + buf[1] = v0.Y; + buf[5] = v1.Y; + buf[2] = v0.Z; + buf[6] = v1.Z; + buf[3] = v0.W; + buf[7] = v1.W; + return buf; + } } From fecdee05bda2de924b4b46aebf7c6f7d9ff3ccdd Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Mon, 22 Jul 2024 19:35:35 +1000 Subject: [PATCH 406/865] Cleanup --- Penumbra/Import/Models/Export/MeshExporter.cs | 17 +- Penumbra/Import/Models/Import/MeshImporter.cs | 31 +- .../Import/Models/Import/VertexAttribute.cs | 272 +++++++----------- 3 files changed, 121 insertions(+), 199 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 20158776..3707ff79 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -314,25 +314,10 @@ public class MeshExporter MdlFile.VertexType.NByte4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f), MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()), MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()), - MdlFile.VertexType.UShort4 => ReadUShort4(reader), + MdlFile.VertexType.UShort4 => reader.ReadBytes(8), var other => throw _notifier.Exception($"Unhandled vertex type {other}"), }; } - - private byte[] ReadUShort4(BinaryReader reader) - { - var buffer = reader.ReadBytes(8); - var byteValues = new byte[8]; - byteValues[0] = buffer[0]; - byteValues[4] = buffer[1]; - byteValues[1] = buffer[2]; - byteValues[5] = buffer[3]; - byteValues[2] = buffer[4]; - byteValues[6] = buffer[5]; - byteValues[3] = buffer[6]; - byteValues[7] = buffer[7]; - return byteValues; - } /// Get the vertex geometry type for this mesh's vertex usages. private Type GetGeometryType(IReadOnlyDictionary usages) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index e3567780..813ef422 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -196,7 +196,9 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // Per glTF specification, an asset with a skin MUST contain skinning attributes on its meshes. var joints0Accessor = primitive.GetVertexAccessor("JOINTS_0")?.AsVector4Array(); var weights0Accessor = primitive.GetVertexAccessor("WEIGHTS_0")?.AsVector4Array(); - + var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); + var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); + if (joints0Accessor == null || weights0Accessor == null) throw notifier.Exception($"Primitive {primitiveIndex} is skinned but does not contain skinning vertex attributes."); @@ -205,6 +207,8 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) { var joints = joints0Accessor[i]; var weights = weights0Accessor[i]; + var joints1 = joints1Accessor?[i]; + var weights1 = weights1Accessor?[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. @@ -212,26 +216,11 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) continue; usedJoints.Add((ushort)joints[index]); - } - } - - var joints1Accessor = primitive.GetVertexAccessor("JOINTS_1")?.AsVector4Array(); - var weights1Accessor = primitive.GetVertexAccessor("WEIGHTS_1")?.AsVector4Array(); - - if (joints1Accessor == null || weights1Accessor == null) - continue; - - for (var i = 0; i < joints1Accessor.Count; i++) - { - var joints = joints1Accessor[i]; - var weights = weights1Accessor[i]; - for (var index = 0; index < 4; index++) - { - // If a joint has absolutely no weight, we omit the bone entirely. - if (weights[index] == 0) - continue; - - usedJoints.Add((ushort)joints[index]); + + if (joints1 != null && weights1 != null && weights1.Value[index] != 0) + { + usedJoints.Add((ushort)joints1.Value[index]); + } } } } diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index b71ad429..12ceba23 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -132,72 +132,50 @@ public class VertexAttribute { if (!accessors.ContainsKey("JOINTS_1")) throw notifier.Exception("Mesh contained WEIGHTS_1 attribute but no corresponding JOINTS_1 attribute."); - - var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - var weights1 = weights1Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => { - var weight0 = weights0[index]; - var weight1 = weights1[index]; - var originalData = BuildUshort4(weight0, weight1); - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - return AdjustByteArray(byteValues, originalData); - } - ); } - else + + var element = new MdlStructs.VertexElement() { - var element = new MdlStructs.VertexElement() + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + var weights1 = weights1Accessor?.AsVector4Array(); + + return new VertexAttribute( + element, + index => { + var weight0 = weights0[index]; + var weight1 = weights1?[index]; + var originalData = BuildUshort4(weight0, weight1 ?? Vector4.Zero); + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + return AdjustByteArray(byteValues, originalData); + } + ); + + /*var element = new MdlStructs.VertexElement() + { + Stream = 0, + Type = (byte)MdlFile.VertexType.NByte4, + Usage = (byte)MdlFile.VertexUsage.BlendWeights, + }; + + var weights0 = weights0Accessor.AsVector4Array(); + + return new VertexAttribute( + element, + index => { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => { - var weight0 = weights0[index]; - var weight1 = Vector4.Zero; - var originalData = BuildUshort4(weight0, weight1); - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - return AdjustByteArray(byteValues, originalData); - } - ); - - /*var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => - { - var weight0 = weights0[index]; - var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - var newByteValues = AdjustByteArray(byteValues, originalData); - if (!newByteValues.SequenceEqual(byteValues)) - notifier.Warning("Adjusted blend weights to maintain precision."); - return newByteValues; - });*/ - } + var weight0 = weights0[index]; + var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; + var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); + var newByteValues = AdjustByteArray(byteValues, originalData); + if (!newByteValues.SequenceEqual(byteValues)) + notifier.Warning("Adjusted blend weights to maintain precision."); + return newByteValues; + });*/ } private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) @@ -241,100 +219,77 @@ public class VertexAttribute if (boneMap == null) throw notifier.Exception("Mesh contained JOINTS_0 attribute but no bone mapping was created."); - var joints0 = joints0Accessor.AsVector4Array(); - var weights0 = weights0Accessor.AsVector4Array(); - - if (accessors.TryGetValue("JOINTS_1", out var joints1Accessor)) + var joints0 = joints0Accessor.AsVector4Array(); + var weights0 = weights0Accessor.AsVector4Array(); + accessors.TryGetValue("JOINTS_1", out var joints1Accessor); + accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor); + var element = new MdlStructs.VertexElement { - if (!accessors.TryGetValue("WEIGHTS_1", out var weights1Accessor)) - throw notifier.Exception("Mesh contained JOINTS_1 attribute but no corresponding WEIGHTS_1 attribute."); + Stream = 0, + Type = (byte)MdlFile.VertexType.UShort4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - var element = new MdlStructs.VertexElement + var joints1 = joints1Accessor?.AsVector4Array(); + var weights1 = weights1Accessor?.AsVector4Array(); + + return new VertexAttribute( + element, + index => { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - var joints1 = joints1Accessor.AsVector4Array(); - var weights1 = weights1Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => + var gltfIndices0 = joints0[index]; + var gltfWeights0 = weights0[index]; + var gltfIndices1 = joints1?[index]; + var gltfWeights1 = weights1?[index]; + var v0 = new Vector4( + gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], + gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], + gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], + gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + ); + + Vector4 v1; + if (gltfIndices1 != null && gltfWeights1 != null) { - var gltfIndices0 = joints0[index]; - var gltfWeights0 = weights0[index]; - var gltfIndices1 = joints1[index]; - var gltfWeights1 = weights1[index]; - var v0 = new Vector4( - gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], - gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], - gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], - gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] + v1 = new Vector4( + gltfWeights1.Value.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.X], + gltfWeights1.Value.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Y], + gltfWeights1.Value.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.Z], + gltfWeights1.Value.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.W] ); - var v1 = new Vector4( - gltfWeights1.X == 0 ? 0 : boneMap[(ushort)gltfIndices1.X], - gltfWeights1.Y == 0 ? 0 : boneMap[(ushort)gltfIndices1.Y], - gltfWeights1.Z == 0 ? 0 : boneMap[(ushort)gltfIndices1.Z], - gltfWeights1.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.W] - ); - - var byteValues = BuildUshort4(v0, v1); - - return byteValues.Select(x => (byte)x).ToArray(); } - ); - } - else + else + { + v1 = Vector4.Zero; + } + + var byteValues = BuildUshort4(v0, v1); + + return byteValues.Select(x => (byte)x).ToArray(); + } + ); + + /*var element = new MdlStructs.VertexElement() { - var element = new MdlStructs.VertexElement - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UShort4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - return new VertexAttribute( - element, - index => - { - var gltfIndices0 = joints0[index]; - var gltfWeights0 = weights0[index]; - var v0 = new Vector4( - gltfWeights0.X == 0 ? 0 : boneMap[(ushort)gltfIndices0.X], - gltfWeights0.Y == 0 ? 0 : boneMap[(ushort)gltfIndices0.Y], - gltfWeights0.Z == 0 ? 0 : boneMap[(ushort)gltfIndices0.Z], - gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] - ); - var v1 = Vector4.Zero; - var byteValues = BuildUshort4(v0, v1); + Stream = 0, + Type = (byte)MdlFile.VertexType.UByte4, + Usage = (byte)MdlFile.VertexUsage.BlendIndices, + }; - return byteValues.Select(x => (byte)x).ToArray(); - } - ); - /*var element = new MdlStructs.VertexElement() + return new VertexAttribute( + element, + index => { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - return new VertexAttribute( - element, - index => - { - var gltfIndices = joints0[index]; - var gltfWeights = weights0[index]; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - );*/ - } + var gltfIndices = joints0[index]; + var gltfWeights = weights0[index]; + return BuildUByte4(new Vector4( + gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], + gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], + gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], + gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] + )); + } + );*/ } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) @@ -620,18 +575,11 @@ public class VertexAttribute (byte)Math.Round(input.Z * 255f), (byte)Math.Round(input.W * 255f), ]; - - private static float[] BuildUshort4(Vector4 v0, Vector4 v1) - { - var buf = new float[8]; - buf[0] = v0.X; - buf[4] = v1.X; - buf[1] = v0.Y; - buf[5] = v1.Y; - buf[2] = v0.Z; - buf[6] = v1.Z; - buf[3] = v0.W; - buf[7] = v1.W; - return buf; - } + + private static float[] BuildUshort4(Vector4 v0, Vector4 v1) => + new[] + { + v0.X, v0.Y, v0.Z, v0.W, + v1.X, v1.Y, v1.Z, v1.W, + }; } From 9de6b3a9055bcd9e80a8c6d694bfbd22d5a37155 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:41:58 +1000 Subject: [PATCH 407/865] Vector4 to float array --- Penumbra/Import/Models/Export/MeshExporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 3707ff79..73160615 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -537,6 +537,7 @@ public class MeshExporter => data switch { byte[] value => value.Select(x => x / 255f).ToArray(), + Vector4 v4 => new[] { v4.X, v4.Y, v4.Z, v4.W }, _ => throw new ArgumentOutOfRangeException($"Invalid float[] input {data}"), }; } From 9c6498e0282aa6626edc243abe4d364e1964e4c2 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:40:47 +1000 Subject: [PATCH 408/865] Conditionally still check for weights1 even if weights0 is 0 --- Penumbra/Import/Models/Import/MeshImporter.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 813ef422..6a46fb9f 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -205,17 +205,18 @@ public class MeshImporter(IEnumerable nodes, IoNotifier notifier) // Build a set of joints that are referenced by this mesh. for (var i = 0; i < joints0Accessor.Count; i++) { - var joints = joints0Accessor[i]; - var weights = weights0Accessor[i]; + var joints0 = joints0Accessor[i]; + var weights0 = weights0Accessor[i]; var joints1 = joints1Accessor?[i]; var weights1 = weights1Accessor?[i]; for (var index = 0; index < 4; index++) { // If a joint has absolutely no weight, we omit the bone entirely. - if (weights[index] == 0) - continue; + if (weights0[index] != 0) + { + usedJoints.Add((ushort)joints0[index]); + } - usedJoints.Add((ushort)joints[index]); if (joints1 != null && weights1 != null && weights1.Value[index] != 0) { From 3e90524b0613fca177414b011ce00b59ac64039c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:40:59 +1000 Subject: [PATCH 409/865] Remove old impl comments --- .../Import/Models/Import/VertexAttribute.cs | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 12ceba23..743ee773 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -154,28 +154,6 @@ public class VertexAttribute return AdjustByteArray(byteValues, originalData); } ); - - /*var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.NByte4, - Usage = (byte)MdlFile.VertexUsage.BlendWeights, - }; - - var weights0 = weights0Accessor.AsVector4Array(); - - return new VertexAttribute( - element, - index => - { - var weight0 = weights0[index]; - var originalData = new[] { weight0.X, weight0.Y, weight0.Z, weight0.W }; - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - var newByteValues = AdjustByteArray(byteValues, originalData); - if (!newByteValues.SequenceEqual(byteValues)) - notifier.Warning("Adjusted blend weights to maintain precision."); - return newByteValues; - });*/ } private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) @@ -268,28 +246,6 @@ public class VertexAttribute return byteValues.Select(x => (byte)x).ToArray(); } ); - - /*var element = new MdlStructs.VertexElement() - { - Stream = 0, - Type = (byte)MdlFile.VertexType.UByte4, - Usage = (byte)MdlFile.VertexUsage.BlendIndices, - }; - - return new VertexAttribute( - element, - index => - { - var gltfIndices = joints0[index]; - var gltfWeights = weights0[index]; - return BuildUByte4(new Vector4( - gltfWeights.X == 0 ? 0 : boneMap[(ushort)gltfIndices.X], - gltfWeights.Y == 0 ? 0 : boneMap[(ushort)gltfIndices.Y], - gltfWeights.Z == 0 ? 0 : boneMap[(ushort)gltfIndices.Z], - gltfWeights.W == 0 ? 0 : boneMap[(ushort)gltfIndices.W] - )); - } - );*/ } public static VertexAttribute? Normal(Accessors accessors, IEnumerable morphAccessors) From 5258c600b7f458aa37d60654378eb58b22372c5c Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:35:11 +1000 Subject: [PATCH 410/865] Rework AdjustByteArray --- .../Import/Models/Import/VertexAttribute.cs | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 743ee773..14a830d4 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -146,43 +146,39 @@ public class VertexAttribute return new VertexAttribute( element, - index => { - var weight0 = weights0[index]; - var weight1 = weights1?[index]; - var originalData = BuildUshort4(weight0, weight1 ?? Vector4.Zero); - var byteValues = originalData.Select(x => (byte)Math.Round(x * 255f)).ToArray(); - return AdjustByteArray(byteValues, originalData); - } + index => BuildBlendWeights(weights0[index], weights1?[index] ?? Vector4.Zero) ); } - - private static byte[] AdjustByteArray(byte[] byteValues, float[] originalValues) + + private static byte[] BuildBlendWeights(Vector4 v1, Vector4 v2) { + var originalData = BuildUshort4(v1, v2); + var byteValues = new byte[originalData.Length]; + for (var i = 0; i < originalData.Length; i++) + { + byteValues[i] = (byte)Math.Round(originalData[i] * 255f); + } + // Blend weights are _very_ sensitive to float imprecision - a vertex sum being off // by one, such as 256, is enough to cause a visible defect. To avoid this, we tweak // the converted values to have the expected sum, preferencing values with minimal differences. - var adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + var adjustment = 255 - byteValues.Sum(value => value); while (adjustment != 0) { - var convertedValues = byteValues.Select(value => value * (1f / 255f)).ToArray(); var closestIndex = Enumerable.Range(0, byteValues.Length) - .Where(index => + .Where(i => adjustment switch { - var byteValue = byteValues[index]; - if (adjustment < 0) - return byteValue > 0; - if (adjustment > 0) - return byteValue < 255; - - return true; + < 0 when byteValues[i] > 0 => true, + > 0 when byteValues[i] < 255 => true, + _ => true, }) - .Select(index => (index, delta: Math.Abs(originalValues[index] - convertedValues[index]))) + .Select(index => (index, delta: Math.Abs(originalData[index] - (byteValues[index] * (1f / 255f))))) .MinBy(x => x.delta) .index; - byteValues[closestIndex] = (byte)(byteValues[closestIndex] + Math.CopySign(1, adjustment)); - adjustment = 255 - byteValues.Select(value => (int)value).Sum(); + byteValues[closestIndex] += (byte)Math.CopySign(1, adjustment); + adjustment = 255 - byteValues.Sum(value => value); } - + return byteValues; } @@ -226,7 +222,7 @@ public class VertexAttribute gltfWeights0.W == 0 ? 0 : boneMap[(ushort)gltfIndices0.W] ); - Vector4 v1; + var v1 = Vector4.Zero; if (gltfIndices1 != null && gltfWeights1 != null) { v1 = new Vector4( @@ -236,10 +232,6 @@ public class VertexAttribute gltfWeights1.Value.W == 0 ? 0 : boneMap[(ushort)gltfIndices1.Value.W] ); } - else - { - v1 = Vector4.Zero; - } var byteValues = BuildUshort4(v0, v1); From 4719f413b6b7c101239490223f90c5e32a5f5de6 Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:42:21 +1000 Subject: [PATCH 411/865] Fix adjustment switch --- Penumbra/Import/Models/Import/VertexAttribute.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra/Import/Models/Import/VertexAttribute.cs b/Penumbra/Import/Models/Import/VertexAttribute.cs index 14a830d4..a1c3246b 100644 --- a/Penumbra/Import/Models/Import/VertexAttribute.cs +++ b/Penumbra/Import/Models/Import/VertexAttribute.cs @@ -168,9 +168,9 @@ public class VertexAttribute var closestIndex = Enumerable.Range(0, byteValues.Length) .Where(i => adjustment switch { - < 0 when byteValues[i] > 0 => true, - > 0 when byteValues[i] < 255 => true, - _ => true, + < 0 => byteValues[i] > 0, + > 0 => byteValues[i] < 255, + _ => true, }) .Select(index => (index, delta: Math.Abs(originalData[index] - (byteValues[index] * (1f / 255f))))) .MinBy(x => x.delta) From 8fa0875ec6bf37e2d48ddde571c77c724c528ccc Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 1 Sep 2024 21:32:31 +1000 Subject: [PATCH 412/865] Fix character*.shpk exports --- .../Import/Models/Export/MaterialExporter.cs | 188 ++++++++++-------- 1 file changed, 107 insertions(+), 81 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 62892473..0f98e5c4 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -36,12 +36,13 @@ public class MaterialExporter return material.Mtrl.ShaderPackage.Name switch { // NOTE: this isn't particularly precise to game behavior (it has some fade around high opacity), but good enough for now. - "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), - "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), - "hair.shpk" => BuildHair(material, name), - "iris.shpk" => BuildIris(material, name), - "skin.shpk" => BuildSkin(material, name), - _ => BuildFallback(material, name, notifier), + "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), + "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "hair.shpk" => BuildHair(material, name), + "iris.shpk" => BuildIris(material, name), + "skin.shpk" => BuildSkin(material, name), + _ => BuildFallback(material, name, notifier), }; } @@ -49,70 +50,65 @@ public class MaterialExporter private static MaterialBuilder BuildCharacter(Material material, string name) { // Build the textures from the color table. - var table = new LegacyColorTable(material.Mtrl.Table!); + var table = new ColorTable(material.Mtrl.Table!); + var indexTexture = material.Textures[(TextureUsage)1449103320]; + var indexOperation = new ProcessCharacterIndexOperation(indexTexture, table); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, indexTexture.Bounds, in indexOperation); - var normal = material.Textures[TextureUsage.SamplerNormal]; + var normalTexture = material.Textures[TextureUsage.SamplerNormal]; + var normalOperation = new ProcessCharacterNormalOperation(normalTexture); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normalTexture.Bounds, in normalOperation); - var operation = new ProcessCharacterNormalOperation(normal, table); - ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation); + // Merge in opacity from the normal. + var baseColor = indexOperation.BaseColor; + MultiplyOperation.Execute(baseColor, normalOperation.BaseColorOpacity); - // Check if full textures are provided, and merge in if available. - var baseColor = operation.BaseColor; + // Check if a full diffuse is provided, and merge in if available. if (material.Textures.TryGetValue(TextureUsage.SamplerDiffuse, out var diffuse)) { - MultiplyOperation.Execute(diffuse, operation.BaseColor); + MultiplyOperation.Execute(diffuse, indexOperation.BaseColor); baseColor = diffuse; } - Image specular = operation.Specular; + var specular = indexOperation.Specular; if (material.Textures.TryGetValue(TextureUsage.SamplerSpecular, out var specularTexture)) { - MultiplyOperation.Execute(specularTexture, operation.Specular); + MultiplyOperation.Execute(specularTexture, indexOperation.Specular); specular = specularTexture; } // Pull further information from the mask. if (material.Textures.TryGetValue(TextureUsage.SamplerMask, out var maskTexture)) { - // Extract the red channel for "ambient occlusion". - maskTexture.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); - maskTexture.ProcessPixelRows(baseColor, (maskAccessor, baseColorAccessor) => - { - for (var y = 0; y < maskAccessor.Height; y++) - { - var maskSpan = maskAccessor.GetRowSpan(y); - var baseColorSpan = baseColorAccessor.GetRowSpan(y); + var maskOperation = new ProcessCharacterMaskOperation(maskTexture); + ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, maskTexture.Bounds, in maskOperation); - for (var x = 0; x < maskSpan.Length; x++) - baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * new Vector4(maskSpan[x].R / 255f)); - } - }); - // TODO: handle other textures stored in the mask? + // TODO: consider using the occusion gltf material property. + MultiplyOperation.Execute(baseColor, maskOperation.Occlusion); + + // Similar to base color's alpha, this is a pretty wasteful operation for a single channel. + MultiplyOperation.Execute(specular, maskOperation.SpecularFactor); } // Specular extension puts colour on RGB and factor on A. We're already packing like that, so we can reuse the texture. var specularImage = BuildImage(specular, name, "specular"); return BuildSharedBase(material, name) - .WithBaseColor(BuildImage(baseColor, name, "basecolor")) - .WithNormal(BuildImage(operation.Normal, name, "normal")) - .WithEmissive(BuildImage(operation.Emissive, name, "emissive"), Vector3.One, 1) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normalOperation.Normal, name, "normal")) + .WithEmissive(BuildImage(indexOperation.Emissive, name, "emissive"), Vector3.One, 1) .WithSpecularFactor(specularImage, 1) .WithSpecularColor(specularImage); } - // TODO: It feels a little silly to request the entire normal here when extracting the normal only needs some of the components. - // As a future refactor, it would be neat to accept a single-channel field here, and then do composition of other stuff later. - // TODO(Dawntrail): Use the dedicated index (_id) map, that is not embedded in the normal map's alpha channel anymore. - private readonly struct ProcessCharacterNormalOperation(Image normal, LegacyColorTable table) : IRowOperation + private readonly struct ProcessCharacterIndexOperation(Image index, ColorTable table) : IRowOperation { - public Image Normal { get; } = normal.Clone(); - public Image BaseColor { get; } = new(normal.Width, normal.Height); - public Image Specular { get; } = new(normal.Width, normal.Height); - public Image Emissive { get; } = new(normal.Width, normal.Height); + public Image BaseColor { get; } = new(index.Width, index.Height); + public Image Specular { get; } = new(index.Width, index.Height); + public Image Emissive { get; } = new(index.Width, index.Height); - private Buffer2D NormalBuffer - => Normal.Frames.RootFrame.PixelBuffer; + private Buffer2D IndexBuffer + => index.Frames.RootFrame.PixelBuffer; private Buffer2D BaseColorBuffer => BaseColor.Frames.RootFrame.PixelBuffer; @@ -125,66 +121,96 @@ public class MaterialExporter public void Invoke(int y) { - var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var indexSpan = IndexBuffer.DangerousGetRowSpan(y); var baseColorSpan = BaseColorBuffer.DangerousGetRowSpan(y); var specularSpan = SpecularBuffer.DangerousGetRowSpan(y); var emissiveSpan = EmissiveBuffer.DangerousGetRowSpan(y); + for (var x = 0; x < indexSpan.Length; x++) + { + ref var indexPixel = ref indexSpan[x]; + + // Calculate and fetch the color table rows being used for this pixel. + var tablePair = (int) Math.Round(indexPixel.R / 17f); + var rowBlend = 1.0f - indexPixel.G / 255f; + + var prevRow = table[tablePair * 2]; + var nextRow = table[Math.Min(tablePair * 2 + 1, ColorTable.NumRows)]; + + // 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)); + + var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend); + specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1)); + + var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend); + emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); + } + } + } + + private readonly struct ProcessCharacterNormalOperation(Image normal) : IRowOperation + { + // TODO: Consider omitting the alpha channel here. + public Image Normal { get; } = normal.Clone(); + // TODO: We only really need the alpha here, however using A8 will result in the multiply later zeroing out the RGB channels. + public Image BaseColorOpacity { get; } = new(normal.Width, normal.Height); + + private Buffer2D NormalBuffer + => Normal.Frames.RootFrame.PixelBuffer; + + private Buffer2D BaseColorOpacityBuffer + => BaseColorOpacity.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) + { + var normalSpan = NormalBuffer.DangerousGetRowSpan(y); + var baseColorOpacitySpan = BaseColorOpacityBuffer.DangerousGetRowSpan(y); + for (var x = 0; x < normalSpan.Length; x++) { ref var normalPixel = ref normalSpan[x]; - // Table row data (.a) - var tableRow = GetTableRowIndices(normalPixel.A / 255f); - var prevRow = table[tableRow.Previous]; - var nextRow = table[tableRow.Next]; + baseColorOpacitySpan[x].FromVector4(Vector4.One); + baseColorOpacitySpan[x].A = normalPixel.B; - // Base colour (table, .b) - var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, tableRow.Weight); - baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1)); - baseColorSpan[x].A = normalPixel.B; - - // Specular (table) - var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, tableRow.Weight); - var lerpedSpecularFactor = float.Lerp((float)prevRow.SpecularMask, (float)nextRow.SpecularMask, tableRow.Weight); - specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, lerpedSpecularFactor)); - - // Emissive (table) - var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, tableRow.Weight); - emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1)); - - // Normal (.rg) - // TODO: we don't actually need alpha at all for normal, but _not_ using the existing rgba texture means I'll need a new one, with a new accessor. Think about it. normalPixel.B = byte.MaxValue; normalPixel.A = byte.MaxValue; } } } - private static TableRow GetTableRowIndices(float input) + private readonly struct ProcessCharacterMaskOperation(Image mask) : IRowOperation { - // These calculations are ported from character.shpk. - var smoothed = MathF.Floor(input * 7.5f % 1.0f * 2) - * (-input * 15 + MathF.Floor(input * 15 + 0.5f)) - + input * 15; + public Image Occlusion { get; } = new(mask.Width, mask.Height); + public Image SpecularFactor { get; } = new(mask.Width, mask.Height); - var stepped = MathF.Floor(smoothed + 0.5f); + private Buffer2D MaskBuffer + => mask.Frames.RootFrame.PixelBuffer; - return new TableRow + private Buffer2D OcclusionBuffer + => Occlusion.Frames.RootFrame.PixelBuffer; + + private Buffer2D SpecularFactorBuffer + => SpecularFactor.Frames.RootFrame.PixelBuffer; + + public void Invoke(int y) { - Stepped = (int)stepped, - Previous = (int)MathF.Floor(smoothed), - Next = (int)MathF.Ceiling(smoothed), - Weight = smoothed % 1, - }; - } + var maskSpan = MaskBuffer.DangerousGetRowSpan(y); + var occlusionSpan = OcclusionBuffer.DangerousGetRowSpan(y); + var specularFactorSpan = SpecularFactorBuffer.DangerousGetRowSpan(y); - private ref struct TableRow - { - public int Stepped; - public int Previous; - public int Next; - public float Weight; + for (var x = 0; x < maskSpan.Length; x++) + { + ref var maskPixel = ref maskSpan[x]; + + occlusionSpan[x].FromL8(new L8(maskPixel.B)); + + specularFactorSpan[x].FromVector4(Vector4.One); + specularFactorSpan[x].A = maskPixel.R; + } + } } private readonly struct MultiplyOperation From 8468ed2c07f808c60c00e1ddd09fd7f9a5aadc06 Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 00:01:30 +1000 Subject: [PATCH 413/865] Fix skin.shpk --- .../Import/Models/Export/MaterialExporter.cs | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 0f98e5c4..ee8484f0 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -340,21 +340,7 @@ public class MaterialExporter var diffuse = material.Textures[TextureUsage.SamplerDiffuse]; var normal = material.Textures[TextureUsage.SamplerNormal]; - // Create a copy of the normal that's the same size as the diffuse for purposes of copying the opacity across. - var resizedNormal = normal.Clone(context => context.Resize(diffuse.Width, diffuse.Height)); - diffuse.ProcessPixelRows(resizedNormal, (diffuseAccessor, normalAccessor) => - { - for (var y = 0; y < diffuseAccessor.Height; y++) - { - var diffuseSpan = diffuseAccessor.GetRowSpan(y); - var normalSpan = normalAccessor.GetRowSpan(y); - - for (var x = 0; x < diffuseSpan.Length; x++) - diffuseSpan[x].A = normalSpan[x].B; - } - }); - - // Clear the blue channel out of the normal now that we're done with it. + // The normal also stores the skin color influence (.b) and wetness mask (.a) - remove. normal.ProcessPixelRows(normalAccessor => { for (var y = 0; y < normalAccessor.Height; y++) @@ -362,7 +348,10 @@ public class MaterialExporter var normalSpan = normalAccessor.GetRowSpan(y); for (var x = 0; x < normalSpan.Length; x++) + { normalSpan[x].B = byte.MaxValue; + normalSpan[x].A = byte.MaxValue; + } } }); From efd08ae0534cd6fc3489e3e605a4e38c52bb040e Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 00:51:51 +1000 Subject: [PATCH 414/865] Add charactertattoo.shpk support --- .../Import/Models/Export/MaterialExporter.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index ee8484f0..31590400 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -39,6 +39,7 @@ public class MaterialExporter "character.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterlegacy.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.MASK, 0.5f), "characterglass.shpk" => BuildCharacter(material, name).WithAlpha(AlphaMode.BLEND), + "charactertattoo.shpk" => BuildCharacterTattoo(material, name), "hair.shpk" => BuildHair(material, name), "iris.shpk" => BuildIris(material, name), "skin.shpk" => BuildSkin(material, name), @@ -244,6 +245,37 @@ public class MaterialExporter } } + private static readonly Vector4 DefaultTattooColor = new Vector4(38, 112, 102, 255) / new Vector4(255); + + private static MaterialBuilder BuildCharacterTattoo(Material material, string name) + { + var normal = material.Textures[TextureUsage.SamplerNormal]; + var baseColor = new Image(normal.Width, normal.Height); + + normal.ProcessPixelRows(baseColor, (normalAccessor, baseColorAccessor) => + { + for (var y = 0; y < normalAccessor.Height; y++) + { + var normalSpan = normalAccessor.GetRowSpan(y); + var baseColorSpan = baseColorAccessor.GetRowSpan(y); + + for (var x = 0; x < normalSpan.Length; x++) + { + baseColorSpan[x].FromVector4(DefaultTattooColor); + baseColorSpan[x].A = normalSpan[x].A; + + normalSpan[x].B = byte.MaxValue; + normalSpan[x].A = byte.MaxValue; + } + } + }); + + return BuildSharedBase(material, name) + .WithBaseColor(BuildImage(baseColor, name, "basecolor")) + .WithNormal(BuildImage(normal, name, "normal")) + .WithAlpha(AlphaMode.BLEND); + } + // TODO: These are hardcoded colours - I'm not keen on supporting highly customizable exports, but there's possibly some more sensible values to use here. private static readonly Vector4 DefaultHairColor = new Vector4(130, 64, 13, 255) / new Vector4(255); private static readonly Vector4 DefaultHighlightColor = new Vector4(77, 126, 240, 255) / new Vector4(255); From 3b21de35cc0e0fac14d4d13600f4adffa2aa60cc Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 01:06:51 +1000 Subject: [PATCH 415/865] Fix iris.shpk --- .../Import/Models/Export/MaterialExporter.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 31590400..bcacf371 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -327,26 +327,23 @@ public class MaterialExporter // NOTE: This is largely the same as the hair material, but is also missing a few features that would cause it to diverge. Keeping separate for now. private static MaterialBuilder BuildIris(Material material, string name) { - var normal = material.Textures[TextureUsage.SamplerNormal]; - var mask = material.Textures[TextureUsage.SamplerMask]; + var normal = material.Textures[TextureUsage.SamplerNormal]; + var mask = material.Textures[TextureUsage.SamplerMask]; + var baseColor = material.Textures[TextureUsage.SamplerDiffuse]; - mask.Mutate(context => context.Resize(normal.Width, normal.Height)); + mask.Mutate(context => context.Resize(baseColor.Width, baseColor.Height)); - var baseColor = new Image(normal.Width, normal.Height); - normal.ProcessPixelRows(mask, baseColor, (normalAccessor, maskAccessor, baseColorAccessor) => + baseColor.ProcessPixelRows(mask, (baseColorAccessor, maskAccessor) => { - for (var y = 0; y < normalAccessor.Height; y++) + for (var y = 0; y < baseColor.Height; y++) { - var normalSpan = normalAccessor.GetRowSpan(y); - var maskSpan = maskAccessor.GetRowSpan(y); var baseColorSpan = baseColorAccessor.GetRowSpan(y); + var maskSpan = maskAccessor.GetRowSpan(y); - for (var x = 0; x < normalSpan.Length; x++) + for (var x = 0; x < baseColorSpan.Length; x++) { - baseColorSpan[x].FromVector4(DefaultEyeColor * new Vector4(maskSpan[x].R / 255f)); - baseColorSpan[x].A = normalSpan[x].A; - - normalSpan[x].A = byte.MaxValue; + var eyeColor = Vector4.Lerp(Vector4.One, DefaultEyeColor, maskSpan[x].B / 255f); + baseColorSpan[x].FromVector4(baseColorSpan[x].ToVector4() * eyeColor); } } }); From a1a880a0f4a85bdbe823e6e972f0bc59e2b8305f Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 2 Sep 2024 01:30:21 +1000 Subject: [PATCH 416/865] Fix hair.shpk --- Penumbra/Import/Models/Export/MaterialExporter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index bcacf371..121e6eed 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -306,10 +306,11 @@ public class MaterialExporter for (var x = 0; x < normalSpan.Length; x++) { - var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, maskSpan[x].A / 255f); - baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].R / 255f)); + var color = Vector4.Lerp(DefaultHairColor, DefaultHighlightColor, normalSpan[x].B / 255f); + baseColorSpan[x].FromVector4(color * new Vector4(maskSpan[x].A / 255f)); baseColorSpan[x].A = normalSpan[x].A; + normalSpan[x].B = byte.MaxValue; normalSpan[x].A = byte.MaxValue; } } From 76c0264cbee424429b7b6c611378015d227fd4c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 14:05:13 +0200 Subject: [PATCH 417/865] Reenable model IO for testing. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 490fa147..de088736 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -97,9 +97,7 @@ public partial class ModEditWindow private void DrawImportExport(MdlTab tab, bool disabled) { - // TODO: Enable when functional. - using var dawntrailDisabled = ImRaii.Disabled(); - if (!ImGui.CollapsingHeader("Import / Export (currently disabled due to Dawntrail format changes)") || true) + if (!ImGui.CollapsingHeader("Import / Export")) return; var childSize = new Vector2((ImGui.GetContentRegionAvail().X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); From df0526e6e510c5129aa0deaf5038728318e5552f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 14:23:36 +0200 Subject: [PATCH 418/865] Fix readoing and displaying DemiHuman IMC Identifiers. --- Penumbra/Meta/Manipulations/Imc.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 1b2492ee..cba6c379 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -68,9 +68,13 @@ public readonly record struct ImcIdentifier( => (MetaIndex)(-1); public override string ToString() - => ObjectType is ObjectType.Equipment or ObjectType.Accessory - ? $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}" - : $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}"; + => ObjectType switch + { + ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}", + ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}", + _ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}", + }; + public bool Validate() { @@ -102,6 +106,7 @@ public readonly record struct ImcIdentifier( return false; if (ItemData.AdaptOffhandImc(PrimaryId, out _)) return false; + break; } @@ -163,7 +168,7 @@ public readonly record struct ImcIdentifier( case ObjectType.DemiHuman: { var secondaryId = new SecondaryId(jObj["SecondaryId"]?.ToObject() ?? 0); - var slot = jObj["Slot"]?.ToObject() ?? EquipSlot.Unknown; + var slot = jObj["EquipSlot"]?.ToObject() ?? EquipSlot.Unknown; ret = new ImcIdentifier(primaryId, (Variant)variant, objectType, secondaryId, slot, BodySlot.Unknown); break; } From 740816f3a670d9f53759d674795456a193ca202d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 6 Oct 2024 14:51:52 +0200 Subject: [PATCH 419/865] Fix accessory VFX change not working. --- .../Hooks/Resources/ResolvePathHooksBase.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index b1b23f27..a31dee4c 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -3,6 +3,8 @@ using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; namespace Penumbra.Interop.Hooks.Resources; @@ -212,25 +214,39 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + [StructLayout(LayoutKind.Explicit)] + private struct ChangedEquipData + { + [FieldOffset(0)] + public PrimaryId Model; + + [FieldOffset(2)] + public Variant Variant; + + [FieldOffset(20)] + public ushort VfxId; + + [FieldOffset(22)] + public GenderRace GenderRace; + } + private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { if (slotIndex is <= 4 or >= 10) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var changedEquipData = ((Human*)drawObject)->ChangedEquipData; + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; // Enable vfxs for accessories if (changedEquipData == null) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var slot = (ushort*)(changedEquipData + 12 * (nint)slotIndex); - var model = slot[0]; - var variant = slot[1]; - var vfxId = slot[4]; + ref var slot = ref changedEquipData[slotIndex]; - if (model == 0 || variant == 0 || vfxId == 0) + if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), $"chara/accessory/a{model:D4}/vfx/eff/va{vfxId:D4}.avfx\0", + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", out _)) return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); From c4b59295cb4db0ab1782fec14137d85b9e6de153 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 6 Oct 2024 12:54:51 +0000 Subject: [PATCH 420/865] [CI] Updating repo.json for testing_1.2.1.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 36b27682..38ea45c0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.5", + "TestingAssemblyVersion": "1.2.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2e424a693d67f16c02e154c28a8f63d9f7056fb2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 7 Oct 2024 16:18:51 +0200 Subject: [PATCH 421/865] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index dd86dafb..34c96a55 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit dd86dafb88ca4c7b662938bbc1310729ba7f788d +Subproject commit 34c96a55efe1ce1296d9edcd8296f6396998cc6a From 4a0c996ff6d492d887a4712606f15e7cf69cb73a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Oct 2024 18:47:30 +0200 Subject: [PATCH 422/865] Fix some off-by-one errors with the import progress reports, add test implementation for pbd editing. --- Penumbra.GameData | 2 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 17 +- Penumbra/Import/TexToolsImporter.Gui.cs | 19 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 + Penumbra/Mods/Editor/ModFileCollection.cs | 18 +- .../AdvancedWindow/ModEditWindow.Deformers.cs | 324 ++++++++++++++++++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 6 + 7 files changed, 368 insertions(+), 20 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 34c96a55..07b01ec9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 34c96a55efe1ce1296d9edcd8296f6396998cc6a +Subproject commit 07b01ec9b043e4b8f56d084f5d6cde1ed4ed9a58 diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 6d4f17b2..f6d1c9eb 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -9,7 +9,6 @@ using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; using Penumbra.Collections.Manager; -using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -28,6 +27,8 @@ public class TemporaryIpcTester( { public Guid LastCreatedCollectionId = Guid.Empty; + private readonly bool _debug = Assembly.GetAssembly(typeof(TemporaryIpcTester))?.GetName().Version?.Major >= 9; + private Guid? _tempGuid; private string _tempCollectionName = string.Empty; private string _tempCollectionGuidName = string.Empty; @@ -48,9 +49,9 @@ public class TemporaryIpcTester( 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); - ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); - ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); + ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); + ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); ImGui.Checkbox("Force Character Collection Overwrite", ref _forceOverwrite); @@ -102,7 +103,7 @@ public class TemporaryIpcTester( !collections.Storage.ByName(_tempModName, out var copyCollection)) && copyCollection is { HasCache: true }) { - var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); + var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); var manips = MetaApi.CompressMetaManipulations(copyCollection); _lastTempError = new AddTemporaryMod(pi).Invoke(_tempModName, guid, files, manips, 999); } @@ -124,11 +125,11 @@ public class TemporaryIpcTester( public void DrawCollections() { - using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections"); + using var collTree = ImUtf8.TreeNode("Temporary Collections##TempCollections"u8); if (!collTree) return; - using var table = ImRaii.Table("##collTree", 6, ImGuiTableFlags.SizingFixedFit); + using var table = ImUtf8.Table("##collTree"u8, 6, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -139,7 +140,7 @@ public class TemporaryIpcTester( var character = tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName) .FirstOrDefault() ?? "Unknown"; - if (ImGui.Button("Save##Collection")) + if (_debug && ImUtf8.Button("Save##Collection"u8)) TemporaryMod.SaveTempCollection(config, saveService, modManager, collection, character); using (ImRaii.PushFont(UiBuilder.MonoFont)) diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index a069204c..f145f560 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -46,21 +46,28 @@ public partial class TexToolsImporter { ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions; - ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}"); + if (_currentOptionIdx >= _currentNumOptions) + ImGui.ProgressBar(1f, size, $"Extracted {_currentNumOptions} Options"); + else + ImGui.ProgressBar(_currentOptionIdx / (float)_currentNumOptions, size, + $"Extracting Option {_currentOptionIdx + 1} / {_currentNumOptions}..."); + ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) ImGui.TextUnformatted( - $"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); + $"Extracting Option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}..."); } ImGui.NewLine(); ImGui.NewLine(); - percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles; - ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}"); + if (_currentFileIdx >= _currentNumFiles) + ImGui.ProgressBar(1f, size, $"Extracted {_currentNumFiles} Files"); + else + ImGui.ProgressBar(_currentFileIdx / (float)_currentNumFiles, size, $"Extracting File {_currentFileIdx + 1} / {_currentNumFiles}..."); + ImGui.NewLine(); if (State != ImporterState.DeduplicatingFiles) - ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); + ImGui.TextUnformatted($"Extracting File {_currentFileName}..."); return false; } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 3ae1eda9..7bbb762e 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -151,6 +151,7 @@ public partial class TexToolsImporter _currentGroupName = string.Empty; _currentOptionName = "Default"; ExtractSimpleModList(_currentModDirectory, modList.SimpleModsList); + ++_currentOptionIdx; } // Iterate through all pages @@ -208,6 +209,7 @@ public partial class TexToolsImporter options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); + ++_currentOptionIdx; } } diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 241f5b3b..20423493 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -12,6 +12,7 @@ public class ModFileCollection : IDisposable, IService private readonly List _mdl = []; private readonly List _tex = []; private readonly List _shpk = []; + private readonly List _pbd = []; private readonly SortedSet _missing = []; private readonly HashSet _usedPaths = []; @@ -23,19 +24,22 @@ public class ModFileCollection : IDisposable, IService => Ready ? _usedPaths : []; public IReadOnlyList Available - => Ready ? _available : Array.Empty(); + => Ready ? _available : []; public IReadOnlyList Mtrl - => Ready ? _mtrl : Array.Empty(); + => Ready ? _mtrl : []; public IReadOnlyList Mdl - => Ready ? _mdl : Array.Empty(); + => Ready ? _mdl : []; public IReadOnlyList Tex - => Ready ? _tex : Array.Empty(); + => Ready ? _tex : []; public IReadOnlyList Shpk - => Ready ? _shpk : Array.Empty(); + => Ready ? _shpk : []; + + public IReadOnlyList Pbd + => Ready ? _pbd : []; public bool Ready { get; private set; } = true; @@ -128,6 +132,9 @@ public class ModFileCollection : IDisposable, IService case ".shpk": _shpk.Add(registry); break; + case ".pbd": + _pbd.Add(registry); + break; } } } @@ -139,6 +146,7 @@ public class ModFileCollection : IDisposable, IService _mdl.Clear(); _tex.Clear(); _shpk.Clear(); + _pbd.Clear(); } private void ClearPaths(bool clearRegistries, CancellationToken tok) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs new file mode 100644 index 00000000..1b6535a7 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -0,0 +1,324 @@ +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; + +namespace Penumbra.UI.AdvancedWindow; + +public partial class ModEditWindow +{ + private readonly FileEditor _pbdTab; + private readonly PbdData _pbdData = new(); + + private bool DrawDeformerPanel(PbdTab tab, bool disabled) + { + _pbdData.Update(tab.File); + DrawGenderRaceSelector(tab); + ImGui.SameLine(); + DrawBoneSelector(); + ImGui.SameLine(); + return DrawBoneData(tab, disabled); + } + + private void DrawGenderRaceSelector(PbdTab tab) + { + using var group = ImUtf8.Group(); + var width = ImUtf8.CalcTextSize("Hellsguard - Female (Child)____0000"u8).X + 2 * ImGui.GetStyle().WindowPadding.X; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(width); + ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8); + } + + using var child = ImUtf8.Child("GenderRace"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + if (!child) + return; + + var metaColor = ColorId.ItemId.Value(); + foreach (var (deformer, index) in tab.File.Deformers.WithIndex()) + { + var name = deformer.GenderRace.ToName(); + var raceCode = deformer.GenderRace.ToRaceCode(); + // No clipping necessary since this are not that many objects anyway. + if (!name.Contains(_pbdData.RaceCodeFilter) && !raceCode.Contains(_pbdData.RaceCodeFilter)) + continue; + + using var id = ImUtf8.PushId(index); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), deformer.RacialDeformer.IsEmpty); + if (ImUtf8.Selectable(name, deformer.GenderRace == _pbdData.SelectedRaceCode)) + { + _pbdData.SelectedRaceCode = deformer.GenderRace; + _pbdData.SelectedDeformer = deformer.RacialDeformer; + } + + ImGui.SameLine(); + color.Push(ImGuiCol.Text, metaColor); + ImUtf8.TextRightAligned(raceCode); + } + } + + private void DrawBoneSelector() + { + using var group = ImUtf8.Group(); + var width = 200 * ImUtf8.GlobalScale; + using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)) + { + ImGui.SetNextItemWidth(width); + ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8); + } + + using var child = ImUtf8.Child("Bone"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + if (!child) + return; + + if (_pbdData.SelectedDeformer == null) + return; + + if (_pbdData.SelectedDeformer.IsEmpty) + { + ImUtf8.Text(""u8); + } + else + { + var height = ImGui.GetTextLineHeightWithSpacing(); + var skips = ImGuiClip.GetNecessarySkips(height); + var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips, + b => b.Contains(_pbdData.BoneFilter), bone + => + { + if (ImUtf8.Selectable(bone, bone == _pbdData.SelectedBone)) + _pbdData.SelectedBone = bone; + }); + ImGuiClip.DrawEndDummy(remainder, height); + } + } + + private bool DrawBoneData(PbdTab tab, bool disabled) + { + using var child = ImUtf8.Child("Data"u8, ImGui.GetContentRegionMax() with { X = ImGui.GetContentRegionAvail().X}, true); + if (!child) + return false; + + if (_pbdData.SelectedBone == null) + return false; + + if (!_pbdData.SelectedDeformer!.DeformMatrices.TryGetValue(_pbdData.SelectedBone, out var matrix)) + return false; + + var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2; + var dummyHeight = ImGui.GetTextLineHeight() / 2; + var ret = DrawAddNewBone(tab, disabled, width); + + ImUtf8.Dummy(0, dummyHeight); + ImGui.Separator(); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawDeformerMatrix(disabled, matrix, width); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawCopyPasteButtons(disabled, matrix, width); + + + ImUtf8.Dummy(0, dummyHeight); + ImGui.Separator(); + ImUtf8.Dummy(0, dummyHeight); + ret |= DrawDecomposedData(disabled, matrix, width); + + return ret; + } + + private bool DrawAddNewBone(PbdTab tab, bool disabled, float width) + { + var ret = false; + ImUtf8.TextFrameAligned("Copy the values of the bone "u8); + ImGui.SameLine(0, 0); + using (ImRaii.PushColor(ImGuiCol.Text, ColorId.NewMod.Value())) + { + ImUtf8.TextFrameAligned(_pbdData.SelectedBone); + } + ImGui.SameLine(0, 0); + ImUtf8.TextFrameAligned(" to a new bone of name"u8); + + var fullWidth = width * 4 + ImGui.GetStyle().ItemSpacing.X * 3; + ImGui.SetNextItemWidth(fullWidth); + ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8); + ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8); + ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X); + if (!ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), + disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null)) + return ret; + + foreach (var deformer in tab.File.Deformers) + { + if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) + continue; + + if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) + && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) + && !newBoneMatrix.Equals(existingMatrix)) + Penumbra.Messager.AddMessage(new Notification( + $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", + NotificationType.Warning)); + else + ret = true; + } + + _pbdData.NewBoneName = string.Empty; + return ret; + } + + private bool DrawDeformerMatrix(bool disabled, in TransformMatrix matrix, float width) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var _ = ImRaii.Disabled(disabled); + var ret = false; + for (var i = 0; i < 3; ++i) + { + for (var j = 0; j < 4; ++j) + { + using var id = ImUtf8.PushId(i * 4 + j); + ImGui.SetNextItemWidth(width); + var tmp = matrix[i, j]; + if (ImUtf8.InputScalar(""u8, ref tmp, "% 12.8f"u8)) + { + ret = true; + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = matrix.ChangeValue(i, j, tmp); + } + + ImGui.SameLine(); + } + + ImGui.NewLine(); + } + + return ret; + } + + private bool DrawCopyPasteButtons(bool disabled, in TransformMatrix matrix, float width) + { + var size = new Vector2(width, 0); + if (ImUtf8.Button("Copy Values"u8, size)) + _pbdData.CopiedMatrix = matrix; + + ImGui.SameLine(); + + if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue)) + { + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value; + return true; + } + + return false; + } + + private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width) + { + var ret = false; + + + if (!matrix.TryDecompose(out var scale, out var rotation, out var translation)) + return false; + + using (ImUtf8.Group()) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var _ = ImRaii.Disabled(disabled); + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleX"u8, ref scale.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleY"u8, ref scale.Y, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##ScaleZ"u8, ref scale.Z, "% 12.8f"u8); + + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationX"u8, ref translation.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationY"u8, ref translation.Y, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##TranslationZ"u8, ref translation.Z, "% 12.8f"u8); + + + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationR"u8, ref rotation.W, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationI"u8, ref rotation.X, "% 12.8f"u8); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationJ"u8, ref rotation.Y, "% 12.8f"u8); + ImGui.SameLine(); + ImGui.SetNextItemWidth(width); + ret |= ImUtf8.InputScalar("##RotationK"u8, ref rotation.Z, "% 12.8f"u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.TextFrameAligned("Scale"u8); + ImUtf8.TextFrameAligned("Translation"u8); + ImUtf8.TextFrameAligned("Rotation (Quaternion, rijk)"u8); + } + + if (ret) + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = TransformMatrix.Compose(scale, rotation, translation); + return ret; + } + + public class PbdTab(byte[] data, string filePath) : IWritable + { + public readonly string FilePath = filePath; + + public readonly PbdFile File = new(data); + + public bool Valid + => File.Valid; + + public byte[] Write() + => File.Write(); + } + + private class PbdData + { + public GenderRace SelectedRaceCode = GenderRace.Unknown; + public RacialDeformer? SelectedDeformer; + public string? SelectedBone; + public string NewBoneName = string.Empty; + public string BoneFilter = string.Empty; + public string RaceCodeFilter = string.Empty; + + public TransformMatrix? CopiedMatrix; + + public void Update(PbdFile file) + { + if (SelectedRaceCode is GenderRace.Unknown) + { + SelectedDeformer = null; + } + else + { + SelectedDeformer = file.Deformers.FirstOrDefault(p => p.GenderRace == SelectedRaceCode).RacialDeformer; + if (SelectedDeformer is null) + SelectedRaceCode = GenderRace.Unknown; + } + } + } +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f2fe8b9e..1a4065bb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -236,6 +236,8 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _itemSwapTab.DrawContent(); } + _pbdTab.Draw(); + DrawMissingFilesTab(); DrawMaterialReassignmentTab(); } @@ -665,6 +667,10 @@ public partial class ModEditWindow : Window, IDisposable, IUiService () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new ShpkTab(_fileDialog, bytes, path)); + _pbdTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Deformers", ".pbd", + () => _editor.Files.Pbd, DrawDeformerPanel, + () => Mod?.ModPath.FullName ?? string.Empty, + (bytes, path, _) => new PbdTab(bytes, path)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; From 40c772a9da9f8b41dad7e023b7a47c48ea8b338e Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 9 Oct 2024 16:49:59 +0000 Subject: [PATCH 423/865] [CI] Updating repo.json for testing_1.2.1.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 38ea45c0..0d9e071f 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.6", + "TestingAssemblyVersion": "1.2.1.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2c5ffc1bc583db158c932ab057a686d6275186a2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Oct 2024 16:50:05 +0200 Subject: [PATCH 424/865] Add delete and single add button and fix child sizes. --- .../AdvancedWindow/ModEditWindow.Deformers.cs | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 1b6535a7..258e51ff 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -38,7 +38,8 @@ public partial class ModEditWindow ImUtf8.InputText("##grFilter"u8, ref _pbdData.RaceCodeFilter, "Filter..."u8); } - using var child = ImUtf8.Child("GenderRace"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + using var child = ImUtf8.Child("GenderRace"u8, + new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true); if (!child) return; @@ -76,7 +77,8 @@ public partial class ModEditWindow ImUtf8.InputText("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8); } - using var child = ImUtf8.Child("Bone"u8, new Vector2(width, ImGui.GetContentRegionMax().Y), true); + using var child = ImUtf8.Child("Bone"u8, + new Vector2(width, ImGui.GetContentRegionMax().Y - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.Y), true); if (!child) return; @@ -104,7 +106,8 @@ public partial class ModEditWindow private bool DrawBoneData(PbdTab tab, bool disabled) { - using var child = ImUtf8.Child("Data"u8, ImGui.GetContentRegionMax() with { X = ImGui.GetContentRegionAvail().X}, true); + using var child = ImUtf8.Child("Data"u8, + ImGui.GetContentRegionAvail() with { Y = ImGui.GetContentRegionMax().Y - ImGui.GetStyle().WindowPadding.Y }, true); if (!child) return false; @@ -116,7 +119,7 @@ public partial class ModEditWindow var width = UiBuilder.MonoFont.GetCharAdvance('0') * 12 + ImGui.GetStyle().FramePadding.X * 2; var dummyHeight = ImGui.GetTextLineHeight() / 2; - var ret = DrawAddNewBone(tab, disabled, width); + var ret = DrawAddNewBone(tab, disabled, matrix, width); ImUtf8.Dummy(0, dummyHeight); ImGui.Separator(); @@ -134,7 +137,7 @@ public partial class ModEditWindow return ret; } - private bool DrawAddNewBone(PbdTab tab, bool disabled, float width) + private bool DrawAddNewBone(PbdTab tab, bool disabled, in TransformMatrix matrix, float width) { var ret = false; ImUtf8.TextFrameAligned("Copy the values of the bone "u8); @@ -143,6 +146,7 @@ public partial class ModEditWindow { ImUtf8.TextFrameAligned(_pbdData.SelectedBone); } + ImGui.SameLine(0, 0); ImUtf8.TextFrameAligned(" to a new bone of name"u8); @@ -151,26 +155,36 @@ public partial class ModEditWindow ImUtf8.InputText("##newBone"u8, ref _pbdData.NewBoneName, "New Bone Name..."u8); ImUtf8.TextFrameAligned("for all races that have a corresponding bone."u8); ImGui.SameLine(0, fullWidth - width - ImGui.GetItemRectSize().X); - if (!ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), + if (ImUtf8.ButtonEx("Apply"u8, ""u8, new Vector2(width, 0), disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedBone == null)) - return ret; - - foreach (var deformer in tab.File.Deformers) { - if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) - continue; + foreach (var deformer in tab.File.Deformers) + { + if (!deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.SelectedBone!, out var existingMatrix)) + continue; - if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) - && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) - && !newBoneMatrix.Equals(existingMatrix)) - Penumbra.Messager.AddMessage(new Notification( - $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", - NotificationType.Warning)); - else - ret = true; + if (!deformer.RacialDeformer.DeformMatrices.TryAdd(_pbdData.NewBoneName, existingMatrix) + && deformer.RacialDeformer.DeformMatrices.TryGetValue(_pbdData.NewBoneName, out var newBoneMatrix) + && !newBoneMatrix.Equals(existingMatrix)) + Penumbra.Messager.AddMessage(new Notification( + $"Could not add deformer matrix to {deformer.GenderRace.ToName()}, Bone {_pbdData.NewBoneName} because it already has a deformer that differs from the intended one.", + NotificationType.Warning)); + else + ret = true; + } + + _pbdData.NewBoneName = string.Empty; } - _pbdData.NewBoneName = string.Empty; + if (ImUtf8.ButtonEx("Copy Values to Single New Bone Entry"u8, ""u8, new Vector2(fullWidth, 0), + disabled || _pbdData.NewBoneName.Length == 0 || _pbdData.SelectedDeformer!.DeformMatrices.ContainsKey(_pbdData.NewBoneName))) + { + _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.NewBoneName] = matrix; + ret = true; + _pbdData.NewBoneName = string.Empty; + } + + return ret; } @@ -209,13 +223,29 @@ public partial class ModEditWindow ImGui.SameLine(); + var ret = false; if (ImUtf8.ButtonEx("Paste Values"u8, ""u8, size, disabled || !_pbdData.CopiedMatrix.HasValue)) { _pbdData.SelectedDeformer!.DeformMatrices[_pbdData.SelectedBone!] = _pbdData.CopiedMatrix!.Value; - return true; + ret = true; } - return false; + var modifier = _config.DeleteModModifier.IsActive(); + ImGui.SameLine(); + if (modifier) + { + if (ImUtf8.ButtonEx("Delete"u8, "Delete this bone entry."u8, size, disabled)) + { + ret |= _pbdData.SelectedDeformer!.DeformMatrices.Remove(_pbdData.SelectedBone!); + _pbdData.SelectedBone = null; + } + } + else + { + ImUtf8.ButtonEx("Delete"u8, $"Delete this bone entry. Hold {_config.DeleteModModifier} to delete.", size, true); + } + + return ret; } private bool DrawDecomposedData(bool disabled, in TransformMatrix matrix, float width) From 1d5a7a41ab9e056d7070d188d01f414debc1f81f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 11 Oct 2024 16:35:47 +0200 Subject: [PATCH 425/865] Remove BonusItem from use and update ResourceTree a bit. --- Penumbra.GameData | 2 +- .../ResolveContext.PathResolution.cs | 8 +- .../Interop/ResourceTree/ResolveContext.cs | 19 ++-- Penumbra/Interop/ResourceTree/ResourceTree.cs | 11 ++- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 94 ++++++++++--------- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 07b01ec9..61e06785 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07b01ec9b043e4b8f56d084f5d6cde1ed4ed9a58 +Subproject commit 61e067857c2cf62bf8426ff6b305e37990f7767a diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 43324516..c554d97a 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -36,13 +36,13 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot); + ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) + : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } private GenderRace ResolveModelRaceCode() - => ResolveEqdpRaceCode(Slot, Equipment.Set); + => ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set); private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) { @@ -161,7 +161,7 @@ internal partial record ResolveContext return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot, variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, Slot.ToSlot(), variant, out var exists); if (!exists) return variant.Id; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index b99ee235..207551e7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -30,7 +30,7 @@ internal record GlobalResolveContext( public readonly Dictionary<(Utf8GamePath, nint), ResourceNode> Nodes = new(128); public unsafe ResolveContext CreateContext(CharaBase* characterBase, uint slotIndex = 0xFFFFFFFFu, - EquipSlot slot = EquipSlot.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) + FullEquipType slot = FullEquipType.Unknown, CharacterArmor equipment = default, SecondaryId secondaryId = default) => new(this, characterBase, slotIndex, slot, equipment, secondaryId); } @@ -38,7 +38,7 @@ internal unsafe partial record ResolveContext( GlobalResolveContext Global, Pointer CharacterBasePointer, uint SlotIndex, - EquipSlot Slot, + FullEquipType Slot, CharacterArmor Equipment, SecondaryId SecondaryId) { @@ -346,13 +346,14 @@ internal unsafe partial record ResolveContext( if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { - var name = Slot switch + var name = item.Name; + if (Slot is FullEquipType.Finger) + name = SlotIndex switch { - EquipSlot.RFinger => "R: ", - EquipSlot.LFinger => "L: ", - _ => string.Empty, - } - + item.Name; + 8 => "R: " + name, + 9 => "L: " + name, + _ => name, + }; return new ResourceNode.UiData(name, item.Type.GetCategoryIcon().ToFlag()); } @@ -361,7 +362,7 @@ internal unsafe partial record ResolveContext( return dataFromPath; return isEquipment - ? new ResourceNode.UiData(Slot.ToName(), Slot.ToEquipType().GetCategoryIcon().ToFlag()) + ? new ResourceNode.UiData(Slot.ToName(), Slot.GetCategoryIcon().ToFlag()) : new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 38f6fe97..246a4508 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -80,12 +80,13 @@ public class ResourceTree { ModelType.Human => i switch { - < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]), - 16 or 17 => globalContext.CreateContext(model, i, EquipSlot.Head, equipment[(int)(i - 6)]), - _ => globalContext.CreateContext(model, i), + < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]), + 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), + 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), + _ => globalContext.CreateContext(model, i), }, _ => i < equipment.Length - ? globalContext.CreateContext(model, i, i.ToEquipSlot(), equipment[(int)i]) + ? globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]) : globalContext.CreateContext(model, i), }; @@ -133,7 +134,7 @@ public class ResourceTree var weapon = (Weapon*)subObject; // This way to tell apart MainHand and OffHand is not always accurate, but seems good enough for what we're doing with it. - var slot = weaponIndex > 0 ? EquipSlot.OffHand : EquipSlot.MainHand; + var slot = weaponIndex > 0 ? FullEquipType.UnknownOffhand : FullEquipType.UnknownMainhand; var equipment = new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, new StainIds(weapon->Stain0, weapon->Stain1)); var weaponType = weapon->SecondaryId; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 361094c4..3aff2ac9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -190,52 +190,6 @@ public class ResourceTreeViewer var frameHeight = ImGui.GetFrameHeight(); var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; - bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) - { - if (!_typeFilter.HasFlag(filterIcon)) - return false; - - if (_nodeFilter.Length == 0) - return true; - - return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) - || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); - } - - NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) - { - if (node.Internal && !debugMode) - return NodeVisibility.Hidden; - - var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; - if (MatchesFilter(node, filterIcon)) - return NodeVisibility.Visible; - - foreach (var child in node.Children) - { - if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) - return NodeVisibility.DescendentsOnly; - } - - return NodeVisibility.Hidden; - } - - NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) - { - if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) - { - visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); - _filterCache.Add(nodePathHash, visibility); - } - - return visibility; - } - - string GetAdditionalDataSuffix(CiByteString data) - => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; - foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { var nodePathHash = unchecked(pathHash + resourceNode.ResourceHandle); @@ -346,6 +300,54 @@ public class ResourceTreeViewer if (unfolded) DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); } + + return; + + string GetAdditionalDataSuffix(CiByteString data) + => !debugMode || data.IsEmpty ? string.Empty : $"\n\nAdditional Data: {data}"; + + NodeVisibility GetNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) + { + if (!_filterCache.TryGetValue(nodePathHash, out var visibility)) + { + visibility = CalculateNodeVisibility(nodePathHash, node, parentFilterIcon); + _filterCache.Add(nodePathHash, visibility); + } + + return visibility; + } + + NodeVisibility CalculateNodeVisibility(nint nodePathHash, ResourceNode node, ChangedItemIconFlag parentFilterIcon) + { + if (node.Internal && !debugMode) + return NodeVisibility.Hidden; + + var filterIcon = node.IconFlag != 0 ? node.IconFlag : parentFilterIcon; + if (MatchesFilter(node, filterIcon)) + return NodeVisibility.Visible; + + foreach (var child in node.Children) + { + if (GetNodeVisibility(unchecked(nodePathHash * 31 + child.ResourceHandle), child, filterIcon) != NodeVisibility.Hidden) + return NodeVisibility.DescendentsOnly; + } + + return NodeVisibility.Hidden; + } + + bool MatchesFilter(ResourceNode node, ChangedItemIconFlag filterIcon) + { + if (!_typeFilter.HasFlag(filterIcon)) + return false; + + if (_nodeFilter.Length == 0) + return true; + + return node.Name != null && node.Name.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.FullName.Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) + || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); + } } [Flags] From db2ce1328ff058548ed159b6ac5908f43a6f2045 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 11 Oct 2024 18:19:12 +0200 Subject: [PATCH 426/865] Enable VFX for the glasses slot. --- Penumbra.GameData | 2 +- .../Hooks/Resources/ResolvePathHooksBase.cs | 61 ++++++++++++++----- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 61e06785..2f6acca6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 61e067857c2cf62bf8426ff6b305e37990f7767a +Subproject commit 2f6acca678b71203763ac4404c3f054747c14f75 diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index a31dee4c..d55caf34 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 static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler; namespace Penumbra.Interop.Hooks.Resources; @@ -223,6 +224,12 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable [FieldOffset(2)] public Variant Variant; + [FieldOffset(8)] + public PrimaryId BonusModel; + + [FieldOffset(10)] + public Variant BonusVariant; + [FieldOffset(20)] public ushort VfxId; @@ -232,26 +239,50 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { - if (slotIndex is <= 4 or >= 10) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + switch (slotIndex) + { + case <= 4: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + case <= 10: + { + // Enable vfxs for accessories + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; - // Enable vfxs for accessories - if (changedEquipData == null) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + ref var slot = ref changedEquipData[slotIndex]; - ref var slot = ref changedEquipData[slotIndex]; + if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (slot.Model == 0 || slot.Variant == 0 || slot.VfxId == 0) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), - $"chara/accessory/a{slot.Model.Id:D4}/vfx/eff/va{slot.VfxId:D4}.avfx\0", - out _)) - return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + *(ulong*)unkOutParam = 4; + return ResolvePath(drawObject, pathBuffer); + } + case 16: + { + // Enable vfxs for glasses + var changedEquipData = (ChangedEquipData*)((Human*)drawObject)->ChangedEquipData; + if (changedEquipData == null) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); - *(ulong*)unkOutParam = 4; - return ResolvePath(drawObject, pathBuffer); + ref var slot = ref changedEquipData[slotIndex - 6]; + + if (slot.BonusModel == 0 || slot.BonusVariant == 0 || slot.VfxId == 0) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + if (!Utf8.TryWrite(new Span((void*)pathBuffer, (int)pathBufferSize), + $"chara/equipment/e{slot.BonusModel.Id:D4}/vfx/eff/ve{slot.VfxId:D4}.avfx\0", + out _)) + return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + + *(ulong*)unkOutParam = 4; + return ResolvePath(drawObject, pathBuffer); + } + default: return ResolveVfx(drawObject, pathBuffer, pathBufferSize, slotIndex, unkOutParam); + } } private nint VFunc81(nint drawObject, int estType, nint unk) From 97b310ca3ffa9397e722a429b8fc2665c4728e37 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 12 Oct 2024 15:13:06 +0200 Subject: [PATCH 427/865] Fix issue with meta file not being saved synchronously on creation. --- Penumbra/Mods/Manager/ModDataEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 933620d9..162f823d 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -37,7 +37,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - saveService.ImmediateSave(new ModMeta(mod)); + saveService.ImmediateSaveSync(new ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) From e646b48afaa58db4d1ac6c19fc7661cc8bbdbcc8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 12 Oct 2024 15:13:22 +0200 Subject: [PATCH 428/865] Add swaps to and from Glasses. --- Penumbra.GameData | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 77 ++++++------ Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 135 ++++++++++++++++------ 3 files changed, 138 insertions(+), 76 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2f6acca6..63cbf824 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2f6acca678b71203763ac4404c3f054747c14f75 +Subproject commit 63cbf824178b5b1f91fd9edc22a6c2bbc2e1cd23 diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 1a2f2798..c7e43a26 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -32,26 +32,26 @@ public static class EquipmentSwap EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot()) throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); var mtrlVariantTo = imcEntry.MaterialId; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -94,7 +94,7 @@ public static class EquipmentSwap { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); if (slotFrom != slotTo) throw new ItemSwap.InvalidItemTypeException(); @@ -111,7 +111,7 @@ public static class EquipmentSwap { (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); @@ -122,18 +122,18 @@ public static class EquipmentSwap { EquipSlot.Head => EstType.Head, EquipSlot.Body => EstType.Body, - _ => (EstType)0, + _ => (EstType)0, }; var skipFemale = false; - var skipMale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -148,7 +148,7 @@ public static class EquipmentSwap swaps.Add(eqdp); var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; - var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); + var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); } @@ -176,7 +176,6 @@ public static class EquipmentSwap return affectedItems; } - public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); @@ -186,9 +185,9 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); - var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); - var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, eqdpFromDefault, eqdpToIdentifier, eqdpToDefault); @@ -217,7 +216,7 @@ public static class EquipmentSwap ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -242,13 +241,13 @@ public static class EquipmentSwap private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var imc = new ImcFile(manager, ident); + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); EquipItem[] items; - Variant[] variants; + Variant[] variants; if (idFrom == idTo) { - items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); + items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); variants = [variantFrom]; } else @@ -271,9 +270,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new GmpIdentifier(idFrom); - var manipToIdentifier = new GmpIdentifier(idTo); - var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -288,9 +287,9 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); @@ -329,7 +328,7 @@ public static class EquipmentSwap var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { @@ -347,9 +346,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new EqpIdentifier(idFrom, slot); - var manipToIdentifier = new EqpIdentifier(idTo, slot); - var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); - var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -381,7 +380,7 @@ public static class EquipmentSwap if (newFileName != fileName) { - fileName = newFileName; + fileName = newFileName; dataWasChanged = true; } @@ -406,13 +405,13 @@ public static class EquipmentSwap EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); - var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } @@ -430,8 +429,8 @@ public static class EquipmentSwap PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); - filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); + filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 6ed1b55d..3f7f2f6c 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -48,15 +48,16 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Glasses] = (new ItemSelector(itemService, selector, FullEquipType.Glasses), new ItemSelector(itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), // @formatter:on }; @@ -129,6 +130,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Ears, Tail, Weapon, + Glasses, } private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) @@ -158,14 +160,14 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private ModSettings? _modSettings; private bool _dirty; - private SwapType _lastTab = SwapType.Hair; - private Gender _currentGender = Gender.Male; - private ModelRace _currentRace = ModelRace.Midlander; - private int _targetId; - private int _sourceId; - private Exception? _loadException; - private EquipSlot _slotFrom = EquipSlot.Head; - private EquipSlot _slotTo = EquipSlot.Ears; + private SwapType _lastTab = SwapType.Hair; + private Gender _currentGender = Gender.Male; + private ModelRace _currentRace = ModelRace.Midlander; + private int _targetId; + private int _sourceId; + private Exception? _loadException; + private BetweenSlotTypes _slotFrom = BetweenSlotTypes.Hat; + private BetweenSlotTypes _slotTo = BetweenSlotTypes.Earrings; private string _newModName = string.Empty; private string _newGroupName = "Swaps"; @@ -200,18 +202,19 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Necklace: case SwapType.Bracelet: case SwapType.Ring: + case SwapType.Glasses: var values = _selectors[_lastTab]; if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown && values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); - break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) - _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item, _slotFrom, selectorFrom.CurrentSelection.Item, + _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom), + selectorFrom.CurrentSelection.Item, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -264,7 +267,23 @@ public class ItemSwapTab : IDisposable, ITab, IUiService } private string CreateDescription() - => $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + { + switch (_lastTab) + { + case SwapType.Ears: + case SwapType.Face: + case SwapType.Hair: + case SwapType.Tail: + return + $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + case SwapType.BetweenSlots: + return + $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}."; + default: + return + $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}."; + } + } private void UpdateOption() { @@ -416,6 +435,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService DrawEquipmentSwap(SwapType.Necklace); DrawEquipmentSwap(SwapType.Bracelet); DrawEquipmentSwap(SwapType.Ring); + DrawEquipmentSwap(SwapType.Glasses); DrawAccessorySwap(); DrawHairSwap(); //DrawFaceSwap(); @@ -454,23 +474,24 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - using (var combo = ImRaii.Combo("##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName())) + using (var combo = ImRaii.Combo("##fromType", ToName(_slotFrom))) { if (combo) - foreach (var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head)) + foreach (var slot in Enum.GetValues()) { - if (!ImGui.Selectable(slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom) || slot == _slotFrom) + if (!ImGui.Selectable(ToName(slot), slot == _slotFrom) || slot == _slotFrom) continue; _dirty = true; _slotFrom = slot; if (slot == _slotTo) - _slotTo = EquipSlotExtensions.AccessorySlots.First(s => slot != s); + _slotTo = AvailableToTypes.First(s => slot != s); } } ImGui.TableNextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, InputWidth * 2 * UiHelpers.Scale, + _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name ?? string.Empty, string.Empty, + InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); (article1, _, selector) = GetAccessorySelector(_slotTo, false); @@ -480,12 +501,12 @@ public class ItemSwapTab : IDisposable, ITab, IUiService ImGui.TableNextColumn(); ImGui.SetNextItemWidth(100 * UiHelpers.Scale); - using (var combo = ImRaii.Combo("##toType", _slotTo.ToName())) + using (var combo = ImRaii.Combo("##toType", ToName(_slotTo))) { if (combo) - foreach (var slot in EquipSlotExtensions.AccessorySlots.Where(s => s != _slotFrom)) + foreach (var slot in AvailableToTypes.Where(t => t != _slotFrom)) { - if (!ImGui.Selectable(slot.ToName(), slot == _slotTo) || slot == _slotTo) + if (!ImGui.Selectable(ToName(slot), slot == _slotTo) || slot == _slotTo) continue; _dirty = true; @@ -508,17 +529,18 @@ public class ItemSwapTab : IDisposable, ITab, IUiService .Select(i => i.Name))); } - private (string, string, ItemSelector) GetAccessorySelector(EquipSlot slot, bool source) + private (string, string, ItemSelector) GetAccessorySelector(BetweenSlotTypes slot, bool source) { var (type, article1, article2) = slot switch { - EquipSlot.Head => (SwapType.Hat, "this", "it"), - EquipSlot.Ears => (SwapType.Earrings, "these", "them"), - EquipSlot.Neck => (SwapType.Necklace, "this", "it"), - EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"), - EquipSlot.RFinger => (SwapType.Ring, "this", "it"), - EquipSlot.LFinger => (SwapType.Ring, "this", "it"), - _ => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.Hat => (SwapType.Hat, "this", "it"), + BetweenSlotTypes.Earrings => (SwapType.Earrings, "these", "them"), + BetweenSlotTypes.Necklace => (SwapType.Necklace, "this", "it"), + BetweenSlotTypes.Bracelets => (SwapType.Bracelet, "these", "them"), + BetweenSlotTypes.RightRing => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.LeftRing => (SwapType.Ring, "this", "it"), + BetweenSlotTypes.Glasses => (SwapType.Glasses, "these", "them"), + _ => (SwapType.Ring, "this", "it"), }; var (itemSelector, target, _, _) = _selectors[type]; return (article1, article2, source ? itemSelector : target); @@ -689,6 +711,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService SwapType.Necklace => "One of the selected necklaces does not seem to exist.", SwapType.Bracelet => "One of the selected bracelets does not seem to exist.", SwapType.Ring => "One of the selected rings does not seem to exist.", + SwapType.Glasses => "One of the selected glasses does not seem to exist.", SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.", SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.", SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.", @@ -746,4 +769,44 @@ public class ItemSwapTab : IDisposable, ITab, IUiService UpdateOption(); _dirty = true; } + + private enum BetweenSlotTypes + { + Hat, + Earrings, + Necklace, + Bracelets, + RightRing, + LeftRing, + Glasses, + } + + private static EquipSlot ToEquipSlot(BetweenSlotTypes type) + => type switch + { + BetweenSlotTypes.Hat => EquipSlot.Head, + BetweenSlotTypes.Earrings => EquipSlot.Ears, + BetweenSlotTypes.Necklace => EquipSlot.Neck, + BetweenSlotTypes.Bracelets => EquipSlot.Wrists, + BetweenSlotTypes.RightRing => EquipSlot.RFinger, + BetweenSlotTypes.LeftRing => EquipSlot.LFinger, + BetweenSlotTypes.Glasses => BonusItemFlag.Glasses.ToEquipSlot(), + _ => EquipSlot.Unknown, + }; + + private static string ToName(BetweenSlotTypes type) + => type switch + { + BetweenSlotTypes.Hat => "Hat", + BetweenSlotTypes.Earrings => "Earrings", + BetweenSlotTypes.Necklace => "Necklace", + BetweenSlotTypes.Bracelets => "Bracelets", + BetweenSlotTypes.RightRing => "Right Ring", + BetweenSlotTypes.LeftRing => "Left Ring", + BetweenSlotTypes.Glasses => "Glasses", + _ => "Unknown", + }; + + private static readonly IReadOnlyList AvailableToTypes = + Enum.GetValues().Where(s => s is not BetweenSlotTypes.Hat).ToArray(); } From a54e45f9c3b59e0532d422230a23c0a2748172b8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 12 Oct 2024 13:15:18 +0000 Subject: [PATCH 429/865] [CI] Updating repo.json for testing_1.2.1.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0d9e071f..4e8b1ed8 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.7", + "TestingAssemblyVersion": "1.2.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 50db83146adab19207e9b867ca8768ae1d7cc06f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Oct 2024 13:55:01 +0200 Subject: [PATCH 430/865] Maybe fix left finger resource nodes. --- .../ResolveContext.PathResolution.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 4 +-- Penumbra/Interop/ResourceTree/ResourceNode.cs | 32 +++++++++---------- Penumbra/Interop/ResourceTree/ResourceTree.cs | 10 +++--- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index c554d97a..79f97881 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -37,7 +37,7 @@ internal partial record ResolveContext { var path = IsEquipmentSlot(SlotIndex) ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()); + : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 207551e7..54612070 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -340,9 +340,9 @@ internal unsafe partial record ResolveContext( internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath) { - var path = gamePath.ToString().Split('/', StringSplitOptions.RemoveEmptyEntries); + var path = gamePath.Path.Split((byte)'/'); // Weapons intentionally left out. - var isEquipment = SafeGet(path, 0) == "chara" && SafeGet(path, 1) is "accessory" or "equipment"; + var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 85d12ce7..6c3e1ebe 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -7,20 +7,20 @@ namespace Penumbra.Interop.ResourceTree; public class ResourceNode : ICloneable { - public string? Name; - public string? FallbackName; - public ChangedItemIconFlag IconFlag; - public readonly ResourceType Type; - public readonly nint ObjectAddress; - public readonly nint ResourceHandle; - public Utf8GamePath[] PossibleGamePaths; - public FullPath FullPath; - public string? ModName; - public string? ModRelativePath; - public CiByteString AdditionalData; - public readonly ulong Length; - public readonly List Children; - internal ResolveContext? ResolveContext; + public string? Name; + public string? FallbackName; + public ChangedItemIconFlag IconFlag; + public readonly ResourceType Type; + public readonly nint ObjectAddress; + public readonly nint ResourceHandle; + public Utf8GamePath[] PossibleGamePaths; + public FullPath FullPath; + public string? ModName; + public string? ModRelativePath; + public CiByteString AdditionalData; + public readonly ulong Length; + public readonly List Children; + internal ResolveContext? ResolveContext; public Utf8GamePath GamePath { @@ -53,7 +53,7 @@ public class ResourceNode : ICloneable { Name = other.Name; FallbackName = other.FallbackName; - IconFlag = other.IconFlag; + IconFlag = other.IconFlag; Type = other.Type; ObjectAddress = other.ObjectAddress; ResourceHandle = other.ResourceHandle; @@ -82,7 +82,7 @@ public class ResourceNode : ICloneable public void SetUiData(UiData uiData) { - Name = uiData.Name; + Name = uiData.Name; IconFlag = uiData.IconFlag; } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 246a4508..89e0c62b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -81,8 +81,8 @@ public class ResourceTree ModelType.Human => i switch { < 10 => globalContext.CreateContext(model, i, i.ToEquipSlot().ToEquipType(), equipment[(int)i]), - 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), - 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), + 16 => globalContext.CreateContext(model, i, FullEquipType.Glasses, equipment[10]), + 17 => globalContext.CreateContext(model, i, FullEquipType.Unknown, equipment[11]), _ => globalContext.CreateContext(model, i), }, _ => i < equipment.Length @@ -185,7 +185,7 @@ public class ResourceTree { pbdNode = pbdNode.Clone(); pbdNode.FallbackName = "Racial Deformer"; - pbdNode.IconFlag = ChangedItemIconFlag.Customization; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(pbdNode); @@ -203,7 +203,7 @@ public class ResourceTree { decalNode = decalNode.Clone(); decalNode.FallbackName = "Face Decal"; - decalNode.IconFlag = ChangedItemIconFlag.Customization; + decalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(decalNode); @@ -220,7 +220,7 @@ public class ResourceTree { legacyDecalNode = legacyDecalNode.Clone(); legacyDecalNode.FallbackName = "Legacy Body Decal"; - legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; + legacyDecalNode.IconFlag = ChangedItemIconFlag.Customization; } Nodes.Add(legacyDecalNode); From 9bd1f86a1d31efe180cc5755852bc6f129e4a187 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 13 Oct 2024 11:57:07 +0000 Subject: [PATCH 431/865] [CI] Updating repo.json for testing_1.2.1.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 4e8b1ed8..2ead369a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.2.1.1", - "TestingAssemblyVersion": "1.2.1.8", + "TestingAssemblyVersion": "1.2.1.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 339d1f8caf00e6afe3fde06ea20685523020887d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 13 Oct 2024 14:41:21 +0200 Subject: [PATCH 432/865] Update GameData --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 63cbf824..554e28a3 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 63cbf824178b5b1f91fd9edc22a6c2bbc2e1cd23 +Subproject commit 554e28a3d1fca9394a20fd9856f6387e2a5e4a57 From 9ddb011545c3b94b7c4958eb47f674627f93bc29 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Oct 2024 16:03:08 +0200 Subject: [PATCH 433/865] Fix issue with long mod titles in the merge mods tab. --- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 26 +++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index b5f0255c..bd62089f 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; @@ -45,9 +46,30 @@ public class ModMergeTab(ModMerger modMerger) : IUiService private void DrawMergeInto(float size) { - using var bigGroup = ImRaii.Group(); + using var bigGroup = ImRaii.Group(); + var minComboSize = 300 * ImGuiHelpers.GlobalScale; + var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X; + ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Merge {modMerger.MergeFromMod!.Name} into "); + + using (ImRaii.Group()) + { + ImUtf8.Text("Merge "u8); + ImGui.SameLine(0, 0); + if (size - textSize < minComboSize) + { + ImUtf8.Text("selected mod"u8, ColorId.FolderLine.Value()); + ImUtf8.HoverTooltip(modMerger.MergeFromMod!.Name.Text); + } + else + { + ImUtf8.Text(modMerger.MergeFromMod!.Name.Text, ColorId.FolderLine.Value()); + } + + ImGui.SameLine(0, 0); + ImUtf8.Text(" into"u8); + } + ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); From 472d803141dab3baa9cf59bef4461986ad1a1f84 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Oct 2024 16:24:30 +0200 Subject: [PATCH 434/865] 1.3.0.0 --- Penumbra/UI/Changelog.cs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 41920d1c..48ac90d8 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -53,10 +53,42 @@ public class PenumbraChangelog : IUiService Add1_1_0_0(Changelog); Add1_1_1_0(Changelog); Add1_2_1_0(Changelog); + Add1_3_0_0(Changelog); } #region Changelogs + private static void Add1_3_0_0(Changelog log) + => log.NextVersion("Version 1.3.0.0") + + .RegisterHighlight("The textures tab in the advanced editing window can now import and export .tga files.") + .RegisterEntry("BC4 and BC6 textures can now also be imported.", 1) + .RegisterHighlight("Added item swapping from and to the Glasses slot.") + .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.", 1) + .RegisterEntry("The import date of a mod is now shown in the Edit Mod tab, and can be reset via button.") + .RegisterEntry("A button to open the file containing local mod data for a mod was also added.", 1) + .RegisterHighlight("IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") + .RegisterEntry("This allows keeping the material index of every IMC entry of a group, while setting the attributes.", 1) + .RegisterHighlight("Model Import/Export was fixed and re-enabled (thanks ackwell and ramen).") + .RegisterHighlight("Added a hack to allow bonus items (face wear, glasses) to have VFX.") + .RegisterEntry("Also fixed the hack that allowed accessories to have VFX not working anymore.", 1) + .RegisterHighlight("Added rudimentary options to edit PBD files in the advanced editing window.") + .RegisterEntry("Preparing the advanced editing window for a mod now does not freeze the game until it is ready.") + .RegisterEntry("Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") + .RegisterEntry("Added a button to the advanced editing window to remove all default-valued meta manipulations from a mod") + .RegisterEntry("Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", 1) + .RegisterEntry("Checkbox-based mod filters are now tri-state checkboxes instead of two disjoint checkboxes.") + .RegisterEntry("Paths from the resource logger can now be copied.") + .RegisterEntry("Silenced some redundant error logs when updating mods via Heliosphere.") + .RegisterEntry("Added 'Page' to imported mod data for TexTools interop. The value is not used in Penumbra, just persisted.") + .RegisterEntry("Updated all external dependencies.") + .RegisterEntry("Fixed issue with Demihuman IMC entries.") + .RegisterEntry("Fixed some off-by-one errors on the mod import window.") + .RegisterEntry("Fixed a race-condition concerning the first-time creation of mod-meta files.") + .RegisterEntry("Fixed an issue with long mod titles in the merge mods tab.") + .RegisterEntry("A bunch of other miscellaneous fixes."); + + private static void Add1_2_1_0(Changelog log) => log.NextVersion("Version 1.2.1.0") .RegisterHighlight("Penumbra is now released for Dawntrail!") From 71101ef553dd19f50678fae379a8f617bb5cd9fb Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 18 Oct 2024 14:26:25 +0000 Subject: [PATCH 435/865] [CI] Updating repo.json for 1.3.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2ead369a..cba274c8 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.2.1.1", - "TestingAssemblyVersion": "1.2.1.9", + "AssemblyVersion": "1.3.0.0", + "TestingAssemblyVersion": "1.3.0.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.2.1.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.2.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 69971c12afd084192dbf40b1f45068d1b6d93916 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Oct 2024 19:23:51 +0200 Subject: [PATCH 436/865] Fix EQP entries for earring hiding. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/GlobalEqpCache.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 554e28a3..e9fc5930 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 554e28a3d1fca9394a20fd9856f6387e2a5e4a57 +Subproject commit e9fc5930a9c035c1e1e3c87ee9bcc4f05eb3015b diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index efcab109..60e782b5 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -40,7 +40,7 @@ public class GlobalEqpCache : ReadWriteDictionary, original |= EqpEntry.HeadShowHrothgarHat; if (_doNotHideEarrings.Contains(armor[5].Set)) - original |= EqpEntry.HeadShowEarrings | EqpEntry.HeadShowEarringsAura | EqpEntry.HeadShowEarringsHuman; + original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura; if (_doNotHideNecklace.Contains(armor[6].Set)) original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; From 7e6ea5008c3f2ee678a8f0505354fb63676d49fd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 17:30:45 +0100 Subject: [PATCH 437/865] Maybe fix other issue with left rings and resource trees. --- Penumbra.GameData | 2 +- .../ResourceTree/ResolveContext.PathResolution.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e9fc5930..e39a04c8 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e9fc5930a9c035c1e1e3c87ee9bcc4f05eb3015b +Subproject commit e39a04c83b67246580492677414888357b5ebed8 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 79f97881..b1cbb74d 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -36,17 +36,16 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot()) + ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()) : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } private GenderRace ResolveModelRaceCode() - => ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set); + => ResolveEqdpRaceCode(SlotIndex, Equipment.Set); - private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId) + private unsafe GenderRace ResolveEqdpRaceCode(uint slotIndex, PrimaryId primaryId) { - var slotIndex = slot.ToIndex(); if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human) return GenderRace.MidlanderMale; @@ -61,6 +60,7 @@ internal partial record ResolveContext var metaCache = Global.Collection.MetaCache; var entry = metaCache?.GetEqdpEntry(characterRaceCode, accessory, primaryId) ?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, characterRaceCode, accessory, primaryId); + var slot = slotIndex.ToEquipSlot(); if (entry.ToBits(slot).Item2) return characterRaceCode; @@ -272,7 +272,7 @@ internal partial record ResolveContext { var human = (Human*)CharacterBase; var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()]; - return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set); + return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot.ToIndex(), equipment.Set), type, equipment.Set); } private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type, From 2358eb378deebd960b16619f243e16fc9a86e845 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 17:31:38 +0100 Subject: [PATCH 438/865] Fix issue with characters in login screen, maybe. --- .../PathResolving/CollectionResolver.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 313c4f8b..1705f871 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using OtterGui; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -62,10 +63,11 @@ public sealed unsafe class CollectionResolver( try { - if (useCache && cache.TryGetValue(gameObject, out var data)) + // Login screen reuses the same actors and can not be cached. + if (LoginScreen(gameObject, out var data)) return data; - if (LoginScreen(gameObject, out data)) + if (useCache && cache.TryGetValue(gameObject, out data)) return data; if (Aesthetician(gameObject, out data)) @@ -116,16 +118,17 @@ public sealed unsafe class CollectionResolver( return true; } - var notYetReady = false; - var lobby = AgentLobby.Instance(); - if (lobby != null) + var notYetReady = false; + var lobby = AgentLobby.Instance(); + var characterList = CharaSelectCharacterList.Instance(); + if (lobby != null && characterList != null) { - var span = lobby->LobbyData.CharaSelectEntries.AsSpan(); // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; - if (idx >= 0 && idx < span.Length && span[idx].Value != null) + if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping) + && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) { - var item = span[idx].Value; + var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); Penumbra.Log.Verbose( $"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}."); @@ -141,7 +144,7 @@ public sealed unsafe class CollectionResolver( var collection = collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? collectionManager.Active.Default; - ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject); + ret = collection.ToResolveData(gameObject); return true; } From c4f6038d1ef629515e830e316ef11dc4871868da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 20:40:10 +0100 Subject: [PATCH 439/865] Make temporary collection always respect ownership. --- .../PathResolving/CollectionResolver.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 1705f871..36c31af3 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -126,7 +126,7 @@ public sealed unsafe class CollectionResolver( // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping) - && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) + && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) { var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); @@ -199,10 +199,24 @@ public sealed unsafe class CollectionResolver( /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) - => tempCollections.Collections.TryGetCollection(identifier, out var collection) - || collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) - ? collection - : null; + { + if (tempCollections.Collections.TryGetCollection(identifier, out var collection)) + return collection; + + // Always inherit ownership for temporary collections. + if (identifier.Type is IdentifierType.Owned) + { + var playerIdentifier = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); + if (tempCollections.Collections.TryGetCollection(playerIdentifier, out collection)) + return collection; + } + + if (collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)) + return collection; + + return null; + } /// Check for the Yourself collection. private ModCollection? CheckYourself(ActorIdentifier identifier, Actor actor) From ed717c69f9676a3314e0235829539ab3a819a5be Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 30 Oct 2024 20:40:10 +0100 Subject: [PATCH 440/865] Make temporary collection always respect ownership. --- .../Manager/IndividualCollections.Access.cs | 6 ++--- .../PathResolving/CollectionResolver.cs | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Penumbra/Collections/Manager/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs index 6b90a333..d0a70630 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -48,8 +48,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa // Handle generic NPC var npcIdentifier = _actors.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, - ushort.MaxValue, - identifier.Kind, identifier.DataId); + ushort.MaxValue, identifier.Kind, identifier.DataId); if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection)) return true; @@ -58,8 +57,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa return false; identifier = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, - identifier.HomeWorld.Id, - ObjectKind.None, uint.MaxValue); + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); return CheckWorlds(identifier, out collection); } case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 1705f871..36c31af3 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -126,7 +126,7 @@ public sealed unsafe class CollectionResolver( // The lobby uses the first 8 cutscene actors. var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index; if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping) - && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) + && lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry)) { var item = charaEntry.Value; var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId); @@ -199,10 +199,24 @@ public sealed unsafe class CollectionResolver( /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) - => tempCollections.Collections.TryGetCollection(identifier, out var collection) - || collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) - ? collection - : null; + { + if (tempCollections.Collections.TryGetCollection(identifier, out var collection)) + return collection; + + // Always inherit ownership for temporary collections. + if (identifier.Type is IdentifierType.Owned) + { + var playerIdentifier = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, + identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue); + if (tempCollections.Collections.TryGetCollection(playerIdentifier, out collection)) + return collection; + } + + if (collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)) + return collection; + + return null; + } /// Check for the Yourself collection. private ModCollection? CheckYourself(ActorIdentifier identifier, Actor actor) From 7dfc564a4cbafd26633aeff3cf8cc37d4a2e2e61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 4 Nov 2024 13:55:45 +0100 Subject: [PATCH 441/865] Add path resolving / est handling for kdb and bnmb files. --- .../Hooks/Resources/ResolvePathHooksBase.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index d55caf34..54066782 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -36,7 +36,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private readonly Hook _resolveMdlPathHook; private readonly Hook _resolveMtrlPathHook; private readonly Hook _resolvePapPathHook; + private readonly Hook _resolveKdbPathHook; private readonly Hook _resolvePhybPathHook; + private readonly Hook _resolveBnmbPathHook; private readonly Hook _resolveSklbPathHook; private readonly Hook _resolveSkpPathHook; private readonly Hook _resolveTmbPathHook; @@ -54,11 +56,10 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _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); @@ -83,7 +84,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook.Enable(); _resolveMtrlPathHook.Enable(); _resolvePapPathHook.Enable(); + _resolveKdbPathHook.Enable(); _resolvePhybPathHook.Enable(); + _resolveBnmbPathHook.Enable(); _resolveSklbPathHook.Enable(); _resolveSkpPathHook.Enable(); _resolveTmbPathHook.Enable(); @@ -101,7 +104,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook.Disable(); _resolveMtrlPathHook.Disable(); _resolvePapPathHook.Disable(); + _resolveKdbPathHook.Disable(); _resolvePhybPathHook.Disable(); + _resolveBnmbPathHook.Disable(); _resolveSklbPathHook.Disable(); _resolveSkpPathHook.Disable(); _resolveTmbPathHook.Disable(); @@ -119,7 +124,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable _resolveMdlPathHook.Dispose(); _resolveMtrlPathHook.Dispose(); _resolvePapPathHook.Dispose(); + _resolveKdbPathHook.Dispose(); _resolvePhybPathHook.Dispose(); + _resolveBnmbPathHook.Dispose(); _resolveSklbPathHook.Dispose(); _resolveSkpPathHook.Dispose(); _resolveTmbPathHook.Dispose(); @@ -149,9 +156,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName) => ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName)); + private nint ResolveKdb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolveKdbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + private nint ResolvePhyb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) => ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + private nint ResolveBnmb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + => ResolvePath(drawObject, _resolveBnmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + private nint ResolveSklb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) => ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); @@ -188,6 +201,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + private nint ResolveKdbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveKdbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); @@ -197,6 +219,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } + private nint ResolveBnmbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) + { + var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); + _parent.MetaState.EstCollection.Push(collection); + var ret = ResolvePath(collection, _resolveBnmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex)); + _parent.MetaState.EstCollection.Pop(); + return ret; + } + private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex) { var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true); From c54141be5489753ce6fa4ad862478615ab25fbae Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 4 Nov 2024 12:59:01 +0000 Subject: [PATCH 442/865] [CI] Updating repo.json for testing_1.3.0.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index cba274c8..686549c9 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.0", + "TestingAssemblyVersion": "1.3.0.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From e3a1ae693813eb06d96d311958aabb5b3abfef55 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 00:50:14 +0100 Subject: [PATCH 443/865] Current state. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../Animation/ApricotListenerSoundPlay.cs | 14 ++++--- .../Interop/Hooks/Animation/SomePapLoad.cs | 2 +- .../Interop/Hooks/Meta/CalculateHeight.cs | 17 ++++---- Penumbra/Interop/Hooks/Meta/UpdateModel.cs | 2 +- .../PostProcessing/ShaderReplacementFixer.cs | 1 - .../PathResolving/CollectionResolver.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- Penumbra/Interop/Services/FontReloader.cs | 2 +- Penumbra/Interop/Services/RedrawService.cs | 4 +- Penumbra/Interop/VolatileOffsets.cs | 35 ++++++++++++++++ .../Manager/OptionEditor/ImcModGroupEditor.cs | 2 +- .../Manager/OptionEditor/ModGroupEditor.cs | 2 +- .../OptionEditor/MultiModGroupEditor.cs | 2 +- .../OptionEditor/SingleModGroupEditor.cs | 2 +- Penumbra/Penumbra.cs | 4 +- Penumbra/Services/MessageService.cs | 6 +-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 ++ Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 41 ++++++++++++++----- 20 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 Penumbra/Interop/VolatileOffsets.cs diff --git a/OtterGui b/OtterGui index 3e6b0857..8ba88eff 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97 +Subproject commit 8ba88eff15326bb28ed5e6157f5252c114d40b5f diff --git a/Penumbra.GameData b/Penumbra.GameData index e39a04c8..fb81a0b5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e39a04c83b67246580492677414888357b5ebed8 +Subproject commit fb81a0b55d3c68f2b26357fac3049c79fb0c22fb diff --git a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs index 44eb7ebb..8838971c 100644 --- a/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs +++ b/Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs @@ -33,25 +33,27 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook> 13) & 1) == 0) + var someIntermediate = *(nint*)(a1 + VolatileOffsets.ApricotListenerSoundPlayCaller.SomeIntermediate); + var flags = someIntermediate == nint.Zero + ? (ushort)0 + : *(ushort*)(someIntermediate + VolatileOffsets.ApricotListenerSoundPlayCaller.Flags); + if (((flags >> VolatileOffsets.ApricotListenerSoundPlayCaller.BitShift) & 1) == 0) return Task.Result.Original(a1, unused, timeOffset); Penumbra.Log.Excessive( $"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}."); // Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay) - var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270); + var apricotIInstanceListenner = *(nint*)(someIntermediate + VolatileOffsets.ApricotListenerSoundPlayCaller.IInstanceListenner); if (apricotIInstanceListenner == nint.Zero) return Task.Result.Original(a1, unused, timeOffset); // In some cases we can obtain the associated caster via vfunc 1. var newData = ResolveData.Invalid; - var gameObject = (*(delegate* unmanaged**)apricotIInstanceListenner)[1](apricotIInstanceListenner); + var gameObject = (*(delegate* unmanaged**)apricotIInstanceListenner)[VolatileOffsets.ApricotListenerSoundPlayCaller.CasterVFunc](apricotIInstanceListenner); if (gameObject != null) { newData = _collectionResolver.IdentifyCollection(gameObject, true); diff --git a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs index 7339c397..f19e4ce2 100644 --- a/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs +++ b/Penumbra/Interop/Hooks/Animation/SomePapLoad.cs @@ -31,7 +31,7 @@ public sealed unsafe class SomePapLoad : FastHook private void Detour(nint a1, int a2, nint a3, int a4) { Penumbra.Log.Excessive($"[Some PAP Load] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}."); - var timelinePtr = a1 + Offsets.TimeLinePtr; + var timelinePtr = a1 + VolatileOffsets.AnimationState.TimeLinePtr; if (timelinePtr != nint.Zero) { var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3); diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index e71d07dd..327e3d1e 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -1,7 +1,7 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using OtterGui.Services; using Penumbra.Interop.PathResolving; -using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Hooks.Meta; @@ -13,19 +13,20 @@ public sealed unsafe class CalculateHeight : FastHook public CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState) { _collectionResolver = collectionResolver; - _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); + _metaState = metaState; + Task = hooks.CreateHook("Calculate Height", (nint)HeightContainer.MemberFunctionPointers.CalculateHeight, Detour, + !HookOverrides.Instance.Meta.CalculateHeight); } - public delegate ulong Delegate(Character* character); + public delegate ulong Delegate(HeightContainer* character); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(Character* character) + private ulong Detour(HeightContainer* container) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)container->OwnerObject, true); _metaState.RspCollection.Push(collection); - var ret = Task.Result.Original.Invoke(character); - Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + var ret = Task.Result.Original.Invoke(container); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)container:X} -> {ret}."); _metaState.RspCollection.Pop(); return ret; } diff --git a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs index 72beea0e..9189ce3b 100644 --- a/Penumbra/Interop/Hooks/Meta/UpdateModel.cs +++ b/Penumbra/Interop/Hooks/Meta/UpdateModel.cs @@ -25,7 +25,7 @@ public sealed unsafe class UpdateModel : FastHook { // Shortcut because this is called all the time. // Same thing is checked at the beginning of the original function. - if (*(int*)((nint)drawObject + Offsets.UpdateModelSkip) == 0) + if (*(int*)((nint)drawObject + VolatileOffsets.UpdateModel.ShortCircuit) == 0) return; Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}."); diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 80892b0f..40958eb4 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -401,7 +401,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private void ModelRendererUnkFuncDetour(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, uint unk3, uint unk4, uint unk5) { - // If we don't have any on-screen instances of modded iris.shpk or others, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForModelRendererUnk() == 0) { _modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 36c31af3..50088008 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -240,7 +240,7 @@ public sealed unsafe class CollectionResolver( } // Only handle human models. - if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId)) + if (!IsModelHuman((uint)actor.AsCharacter->ModelCharaId)) return null; if (actor.Customize->Data[0] == 0) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 89e0c62b..62f4febe 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -68,7 +68,7 @@ public class ResourceTree Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), _ => [], }; - ModelId = character->CharacterData.ModelCharaId; + ModelId = character->ModelCharaId; CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; diff --git a/Penumbra/Interop/Services/FontReloader.cs b/Penumbra/Interop/Services/FontReloader.cs index 4f48f08f..3a2c7022 100644 --- a/Penumbra/Interop/Services/FontReloader.cs +++ b/Penumbra/Interop/Services/FontReloader.cs @@ -43,7 +43,7 @@ public unsafe class FontReloader : IService return; _atkModule = &atkModule->AtkModule; - _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->VirtualTable)[Offsets.ReloadFontsVfunc]; + _reloadFontsFunc = ((delegate* unmanaged*)_atkModule->VirtualTable)[VolatileOffsets.FontReloader.ReloadFontsVFunc]; }); } } diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 2cdc1137..8f20ca5e 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -38,10 +38,10 @@ public unsafe partial class RedrawService : IService // VFuncs that disable and enable draw, used only for GPose actors. private static void DisableDraw(IGameObject actor) - => ((delegate* unmanaged**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); + => ((delegate* unmanaged**)actor.Address)[0][VolatileOffsets.RedrawService.DisableDrawVFunc](actor.Address); private static void EnableDraw(IGameObject actor) - => ((delegate* unmanaged**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); + => ((delegate* unmanaged**)actor.Address)[0][VolatileOffsets.RedrawService.EnableDrawVFunc](actor.Address); // Check whether we currently are in GPose. // Also clear the name list. diff --git a/Penumbra/Interop/VolatileOffsets.cs b/Penumbra/Interop/VolatileOffsets.cs new file mode 100644 index 00000000..2c6e3180 --- /dev/null +++ b/Penumbra/Interop/VolatileOffsets.cs @@ -0,0 +1,35 @@ +namespace Penumbra.Interop; + +public static class VolatileOffsets +{ + public static class ApricotListenerSoundPlayCaller + { + public const int PlayTimeOffset = 0x254; + public const int SomeIntermediate = 0x1F8; + public const int Flags = 0x4A4; + public const int IInstanceListenner = 0x270; + public const int BitShift = 13; + public const int CasterVFunc = 1; + } + + public static class AnimationState + { + public const int TimeLinePtr = 0x50; + } + + public static class UpdateModel + { + public const int ShortCircuit = 0xA2C; + } + + public static class FontReloader + { + public const int ReloadFontsVFunc = 43; + } + + public static class RedrawService + { + public const int EnableDrawVFunc = 12; + public const int DisableDrawVFunc = 13; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index dc94c881..f8760625 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -138,7 +138,7 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) return false; group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index 7f18852d..d01297db 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -90,7 +90,7 @@ public class ModGroupEditor( { var mod = group.Mod; var idxFrom = group.GetIndex(); - if (!mod.Groups.Move(idxFrom, groupIdxTo)) + if (!mod.Groups.Move(ref idxFrom, ref groupIdxTo)) return; saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); diff --git a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index 74362325..2446ae80 100644 --- a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -75,7 +75,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) return false; group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); diff --git a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 15a899a0..5fd785cf 100644 --- a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -48,7 +48,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) return false; group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b6b19ef2..41d8f668 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,6 +1,5 @@ using Dalamud.Plugin; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Log; using OtterGui.Services; @@ -20,6 +19,7 @@ using OtterGui.Tasks; using Penumbra.UI; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Dalamud.Plugin.Services; +using Lumina.Excel.Sheets; using Penumbra.GameData.Data; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; @@ -111,7 +111,7 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { _services.GetService(); - var itemSheet = _services.GetService().GetExcelSheet()!; + var itemSheet = _services.GetService().GetExcelSheet(); _communicatorService.ChangedItemHover.Subscribe(it => { if (it is IdentifiedItem { Item.Id.IsItem: true }) diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index a35a67f1..e610cb6a 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -4,7 +4,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; -using Lumina.Excel.GeneratedSheets; +using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; using Penumbra.Mods.Manager; @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager) : OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService { - public void LinkItem(Item item) + public void LinkItem(in Item item) { // @formatter:off var payloadList = new List @@ -29,7 +29,7 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti new TextPayload($"{(char)SeIconChar.LinkMarker}"), new UIForegroundPayload(0), new UIGlowPayload(0), - new TextPayload(item.Name), + new TextPayload(item.Name.ExtractText()), new RawPayload([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]), new RawPayload([0x02, 0x13, 0x02, 0xEC, 0x03]), }; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 7c6cd01e..47c2c16c 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -54,6 +54,9 @@ public class Diagnostics(ServiceManager provider) : IUiService return; using var table = ImRaii.Table("##data", 4, ImGuiTableFlags.RowBg); + if (!table) + return; + foreach (var type in typeof(ActorManager).Assembly.GetTypes() .Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer)))) { diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs index 7af1f884..e8ff9b9c 100644 --- a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -34,28 +34,49 @@ public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : IUiSe Penumbra.Log.Error($"Could not delete hook override file at {path}:\n{ex}"); } + bool? allVisible = null; + ImGui.SameLine(); + if (ImUtf8.Button("Disable All Visible Hooks"u8)) + allVisible = true; + ImGui.SameLine(); + if (ImUtf8.Button("Enable All VisibleHooks"u8)) + allVisible = false; + bool? all = null; ImGui.SameLine(); - if (ImUtf8.Button("Disable All Hooks"u8)) + if (ImUtf8.Button("Disable All Hooks")) all = true; ImGui.SameLine(); - if (ImUtf8.Button("Enable All Hooks"u8)) + if (ImUtf8.Button("Enable All Hooks")) all = false; foreach (var propertyField in typeof(HookOverrides).GetFields().Where(f => f is { IsStatic: false, FieldType.IsValueType: true })) { using var tree = ImUtf8.TreeNode(propertyField.Name); if (!tree) - continue; - - var property = propertyField.GetValue(_overrides); - foreach (var valueField in propertyField.FieldType.GetFields()) { - var value = valueField.GetValue(property) as bool? ?? false; - if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || all.HasValue) + if (all.HasValue) { - valueField.SetValue(property, all ?? value); - propertyField.SetValue(_overrides, property); + var property = propertyField.GetValue(_overrides); + foreach (var valueField in propertyField.FieldType.GetFields()) + { + valueField.SetValue(property, all.Value); + propertyField.SetValue(_overrides, property); + } + } + } + else + { + allVisible ??= all; + var property = propertyField.GetValue(_overrides); + foreach (var valueField in propertyField.FieldType.GetFields()) + { + var value = valueField.GetValue(property) as bool? ?? false; + if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || allVisible.HasValue) + { + valueField.SetValue(property, allVisible ?? value); + propertyField.SetValue(_overrides, property); + } } } } From 5599f12753624981a79609bbc17a283e24bd0dc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 14:28:33 +0100 Subject: [PATCH 444/865] Further fixes. --- Penumbra/Interop/Hooks/Meta/CalculateHeight.cs | 12 ++++++------ Penumbra/Interop/PathResolving/CollectionResolver.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceTree.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 327e3d1e..0e85b3ae 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -14,19 +14,19 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)HeightContainer.MemberFunctionPointers.CalculateHeight, Detour, + Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); } - public delegate ulong Delegate(HeightContainer* character); + public delegate ulong Delegate(Character* character); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(HeightContainer* container) + private ulong Detour(Character* character) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)container->OwnerObject, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); _metaState.RspCollection.Push(collection); - var ret = Task.Result.Original.Invoke(container); - Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)container:X} -> {ret}."); + var ret = Task.Result.Original.Invoke(character); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); _metaState.RspCollection.Pop(); return ret; } diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 50088008..576b61bb 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -240,7 +240,7 @@ public sealed unsafe class CollectionResolver( } // Only handle human models. - if (!IsModelHuman((uint)actor.AsCharacter->ModelCharaId)) + if (!IsModelHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) return null; if (actor.Customize->Data[0] == 0) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 62f4febe..b50fc695 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -68,7 +68,7 @@ public class ResourceTree Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10), _ => [], }; - ModelId = character->ModelCharaId; + ModelId = character->ModelContainer.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; From 83e5feb7dbbe452d8499562733dc9ba0edcee1bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 14:30:47 +0100 Subject: [PATCH 445/865] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 8ba88eff..95b8d177 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 8ba88eff15326bb28ed5e6157f5252c114d40b5f +Subproject commit 95b8d177883b03f804d77434f45e9de97fdb9adf From a864ac196550776a4870b7fc646591a4f1d55632 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Nov 2024 22:00:07 +0100 Subject: [PATCH 446/865] 1.3.0.2 --- .github/workflows/test_release.yml | 2 +- Penumbra/Penumbra.json | 2 +- repo.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 0718ded2..549c967a 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_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: | diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 805f4d85..4790da18 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 10, + "DalamudApiLevel": 11, "LoadPriority": 69420, "LoadState": 2, "LoadSync": true, diff --git a/repo.json b/repo.json index 686549c9..71d8fad4 100644 --- a/repo.json +++ b/repo.json @@ -10,7 +10,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, - "TestingDalamudApiLevel": 10, + "TestingDalamudApiLevel": 11, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 597380355a4a769de852a0e4e03656163f34fa56 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 17 Nov 2024 21:02:11 +0000 Subject: [PATCH 447/865] [CI] Updating repo.json for testing_1.3.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 71d8fad4..651802a6 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.1", + "TestingAssemblyVersion": "1.3.0.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 7ab5299f7ae77c93e5f318529d1071ec3dfd4931 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Nov 2024 00:28:27 +0100 Subject: [PATCH 448/865] Add SCD handling and crc cache visualization. --- Penumbra.sln | 3 + .../Hooks/ResourceLoading/ResourceLoader.cs | 8 +-- .../Hooks/ResourceLoading/TexMdlService.cs | 62 ++++++++++++++----- Penumbra/UI/Tabs/Debug/DebugTab.cs | 29 ++++++++- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 46609f85..94a04ef3 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -8,7 +8,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\release.yml = .github\workflows\release.yml repo.json = repo.json + .github\workflows\test_release.yml = .github\workflows\test_release.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}" diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index bcd09b37..442bac15 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -15,18 +15,18 @@ public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; - private readonly TexMdlService _texMdlService; + private readonly TexMdlScdService _texMdlScdService; private readonly PapHandler _papHandler; private readonly Configuration _config; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, Configuration config) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlScdService texMdlScdService, Configuration config) { _resources = resources; _fileReadService = fileReadService; - _texMdlService = texMdlService; + _texMdlScdService = texMdlScdService; _config = config; ResetResolvePath(); @@ -140,7 +140,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; } - _texMdlService.AddCrc(type, resolvedPath); + _texMdlScdService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index d4a2dfba..9c17e0cf 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -10,7 +10,7 @@ using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.Re namespace Penumbra.Interop.Hooks.ResourceLoading; -public unsafe class TexMdlService : IDisposable, IRequiredService +public unsafe class TexMdlScdService : IDisposable, IRequiredService { /// /// We need to be able to obtain the requested LoD level. @@ -42,7 +42,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private readonly LodService _lodService; - public TexMdlService(IGameInteropProvider interop) + public TexMdlScdService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); @@ -52,6 +52,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService _loadMdlFileExternHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); + _soundOnLoadHook.Enable(); } /// Add CRC64 if the given file is a model or texture file and has an associated path. @@ -59,8 +60,9 @@ public unsafe class TexMdlService : IDisposable, IRequiredService { _ = type switch { - ResourceType.Mdl when path.HasValue => _customMdlCrc.Add(path.Value.Crc64), - ResourceType.Tex when path.HasValue => _customTexCrc.Add(path.Value.Crc64), + ResourceType.Mdl when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Mdl), + ResourceType.Tex when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Tex), + ResourceType.Scd when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Scd), _ => false, }; } @@ -70,15 +72,16 @@ public unsafe class TexMdlService : IDisposable, IRequiredService _checkFileStateHook.Dispose(); _loadMdlFileExternHook.Dispose(); _textureOnLoadHook.Dispose(); + _soundOnLoadHook.Dispose(); } /// /// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files, /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// - private readonly HashSet _customMdlCrc = []; - - private readonly HashSet _customTexCrc = []; + private readonly Dictionary _customFileCrc = []; + public IReadOnlyDictionary CustomCache + => _customFileCrc; private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64); @@ -86,12 +89,34 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private readonly Hook _checkFileStateHook = null!; private readonly ThreadLocal _texReturnData = new(() => default); + private readonly ThreadLocal _scdReturnData = new(() => default); private delegate void UpdateCategoryDelegate(TextureResourceHandle* resourceHandle); [Signature(Sigs.TexHandleUpdateCategory)] private readonly UpdateCategoryDelegate _updateCategory = null!; + private delegate byte SoundOnLoadDelegate(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk); + + [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 8B 79 ?? 48 8B DA 8B D7")] + private readonly delegate* unmanaged _loadScdFileLocal = null!; + + [Signature("40 56 57 41 54 48 81 EC 90 00 00 00 80 3A 0B 45 0F B6 E0 48 8B F2", DetourName = nameof(OnScdLoadDetour))] + private readonly Hook _soundOnLoadHook = null!; + + private byte OnScdLoadDetour(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk) + { + var ret = _soundOnLoadHook.Original(handle, descriptor, unk); + if (!_scdReturnData.Value) + return ret; + + // Function failed on a replaced scd, call local. + _scdReturnData.Value = false; + ret = _loadScdFileLocal(handle, descriptor, unk); + _updateCategory((TextureResourceHandle*)handle); + return ret; + } + /// /// The function that checks a files CRC64 to determine whether it is 'protected'. /// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models. @@ -100,14 +125,17 @@ public unsafe class TexMdlService : IDisposable, IRequiredService /// private nint CheckFileStateDetour(nint ptr, ulong crc64) { - if (_customMdlCrc.Contains(crc64)) - return CustomFileFlag; - - if (_customTexCrc.Contains(crc64)) - { - _texReturnData.Value = true; - return nint.Zero; - } + if (_customFileCrc.TryGetValue(crc64, out var type)) + switch (type) + { + case ResourceType.Mdl: return CustomFileFlag; + case ResourceType.Tex: + _texReturnData.Value = true; + return nint.Zero; + case ResourceType.Scd: + _scdReturnData.Value = true; + return nint.Zero; + } var ret = _checkFileStateHook.Original(ptr, crc64); Penumbra.Log.Excessive($"[CheckFileState] Called on 0x{ptr:X} with CRC {crc64:X16}, returned 0x{ret:X}."); @@ -128,10 +156,10 @@ public unsafe class TexMdlService : IDisposable, IRequiredService private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2); - [Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnLoadDetour))] + [Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnTexLoadDetour))] private readonly Hook _textureOnLoadHook = null!; - private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) + private byte OnTexLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2) { var ret = _textureOnLoadHook.Original(handle, descriptor, unk2); if (!_texReturnData.Value) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 47c2c16c..9184ffe8 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -102,6 +102,7 @@ public class DebugTab : Window, ITab, IUiService private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; private readonly HookOverrideDrawer _hookOverrides; + private readonly TexMdlScdService _texMdlScdService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, @@ -112,7 +113,7 @@ public class DebugTab : Window, ITab, IUiService CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides) + HookOverrideDrawer hookOverrides, TexMdlScdService texMdlScdService) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -150,6 +151,7 @@ public class DebugTab : Window, ITab, IUiService _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; _hookOverrides = hookOverrides; + _texMdlScdService = texMdlScdService; _objects = objects; _clientState = clientState; } @@ -183,6 +185,7 @@ public class DebugTab : Window, ITab, IUiService DrawDebugCharacterUtility(); DrawShaderReplacementFixer(); DrawData(); + DrawCrcCache(); DrawResourceProblems(); _hookOverrides.Draw(); DrawPlayerModelInfo(); @@ -1021,6 +1024,30 @@ public class DebugTab : Window, ITab, IUiService DrawDebugResidentResources(); } + private unsafe void DrawCrcCache() + { + var header = ImUtf8.CollapsingHeader("CRC Cache"u8); + if (!header) + return; + + using var table = ImUtf8.Table("table"u8, 2); + if (!table) + return; + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.TableSetupColumn("Hash"u8, ImGuiTableColumnFlags.WidthFixed, 18 * UiBuilder.MonoFont.GetCharAdvance('0')); + ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0')); + ImGui.TableHeadersRow(); + + foreach (var (hash, type) in _texMdlScdService.CustomCache) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"{hash:X16}"); + ImGui.TableNextColumn(); + ImUtf8.Text($"{type}"); + } + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() { From 41718d8f8fd9106136691f14588fab33b3073269 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Nov 2024 00:28:36 +0100 Subject: [PATCH 449/865] Fix screen actor indices. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fb81a0b5..79d8d782 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fb81a0b55d3c68f2b26357fac3049c79fb0c22fb +Subproject commit 79d8d782b3b454a41f7f87f398806ec4d08d485f From 0928d712c91e7ea893c8088d51af63221be486fe Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 17 Nov 2024 23:30:41 +0000 Subject: [PATCH 450/865] [CI] Updating repo.json for testing_1.3.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 651802a6..77f5012e 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.2", + "TestingAssemblyVersion": "1.3.0.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8a53313c33d0a42edfbbdf09a141525f7db14794 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Nov 2024 20:14:06 +0100 Subject: [PATCH 451/865] Add .atch file debugging. --- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 56 ++++++++++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 29 +++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/AtchDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 79d8d782..1c82c086 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 79d8d782b3b454a41f7f87f398806ec4d08d485f +Subproject commit 1c82c086704e2f1b3608644a9b1d70628fbe0ca9 diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs new file mode 100644 index 00000000..f6f6c50e --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -0,0 +1,56 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Text; +using Penumbra.GameData.Files; + +namespace Penumbra.UI.Tabs.Debug; + +public static class AtchDrawer +{ + public static void Draw(AtchFile file) + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Entries: "u8); + ImUtf8.Text("States: "u8); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{file.Entries.Count}"); + if (file.Entries.Count == 0) + { + ImUtf8.Text("0"u8); + return; + } + + ImUtf8.Text($"{file.Entries[0].States.Count}"); + } + + foreach (var (entry, index) in file.Entries.WithIndex()) + { + using var id = ImUtf8.PushId(index); + using var tree = ImUtf8.TreeNode(entry.Name.Span); + if (tree) + { + ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + foreach (var (state, i) in entry.States.WithIndex()) + { + id.Push(i); + using var t = ImUtf8.TreeNode(state.Bone.Span); + if (t) + { + ImUtf8.TreeNode($"Scale: {state.Scale}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"Offset: {state.Offset.X} | {state.Offset.Y} | {state.Offset.Z}", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + ImUtf8.TreeNode($"Rotation: {state.Rotation.X} | {state.Rotation.Y} | {state.Rotation.Z}", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } + + id.Pop(); + } + } + } + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9184ffe8..cdaaadaa 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -98,6 +98,7 @@ public class DebugTab : Window, ITab, IUiService private readonly Diagnostics _diagnostics; private readonly ObjectManager _objects; private readonly IClientState _clientState; + private readonly IDataManager _dataManager; private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; @@ -105,7 +106,7 @@ public class DebugTab : Window, ITab, IUiService private readonly TexMdlScdService _texMdlScdService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, - IClientState clientState, + IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, CollectionResolver collectionResolver, @@ -154,6 +155,7 @@ public class DebugTab : Window, ITab, IUiService _texMdlScdService = texMdlScdService; _objects = objects; _clientState = clientState; + _dataManager = dataManager; } public ReadOnlySpan Label @@ -665,11 +667,36 @@ public class DebugTab : Window, ITab, IUiService DrawEmotes(); DrawStainTemplates(); + DrawAtch(); } private string _emoteSearchFile = string.Empty; private string _emoteSearchName = string.Empty; + + private AtchFile? _atchFile; + + private void DrawAtch() + { + try + { + _atchFile ??= new AtchFile(_dataManager.GetFile("chara/xls/attachOffset/c0101.atch")!.Data); + } + catch + { + // ignored + } + + if (_atchFile == null) + return; + + using var mainTree = ImUtf8.TreeNode("Atch File C0101"u8); + if (!mainTree) + return; + + AtchDrawer.Draw(_atchFile); + } + private void DrawEmotes() { using var mainTree = TreeNode("Emotes"); From 3beef61c6f04e6bc40ffb40be998d8cf14546cee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Nov 2024 10:44:09 +0100 Subject: [PATCH 452/865] Some debug vis improvements, disable .atch file modding for the moment until modular .atch file modding is implemented. --- .../Interop/PathResolving/PathResolver.cs | 4 ++ Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 51 +++++++++++++------ 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 63bbc8d8..a7af42e3 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -52,6 +52,10 @@ public class PathResolver : IDisposable, IService if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) return (null, ResolveData.Invalid); + // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. + if (resourceType is ResourceType.Atch) + return (null, ResolveData.Invalid); + return category switch { // Only Interface collection. diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index f6f6c50e..3e407e99 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -31,7 +31,7 @@ public static class AtchDrawer foreach (var (entry, index) in file.Entries.WithIndex()) { using var id = ImUtf8.PushId(index); - using var tree = ImUtf8.TreeNode(entry.Name.Span); + using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Name.Span}"); if (tree) { ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index cdaaadaa..28911b05 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,7 @@ using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; namespace Penumbra.UI.Tabs.Debug; @@ -196,7 +197,7 @@ public class DebugTab : Window, ITab, IUiService } - private void DrawCollectionCaches() + private unsafe void DrawCollectionCaches() { if (!ImGui.CollapsingHeader( $"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections")) @@ -207,25 +208,35 @@ public class DebugTab : Window, ITab, IUiService if (collection.HasCache) { using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})"); + using var node = TreeNode($"{collection.Name} (Change Counter {collection.ChangeCounter})###{collection.Name}"); if (!node) continue; color.Pop(); - foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) + using (var resourceNode = ImUtf8.TreeNode("Custom Resources"u8)) { - using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name); - using var node2 = TreeNode(mod.Name.Text); - if (!node2) - continue; - - foreach (var path in paths) - - TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - - foreach (var manip in manips) - TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + if (resourceNode) + foreach (var (path, resource) in collection._cache!.CustomResources) + ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}", + ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } + + using var modNode = ImUtf8.TreeNode("Enabled Mods"u8); + if (modNode) + foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) + { + using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name); + using var node2 = TreeNode(mod.Name.Text); + if (!node2) + continue; + + foreach (var path in paths) + + TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + + foreach (var manip in manips) + TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } } else { @@ -1051,17 +1062,27 @@ public class DebugTab : Window, ITab, IUiService DrawDebugResidentResources(); } + private string _crcInput = string.Empty; + private FullPath _crcPath = FullPath.Empty; + private unsafe void DrawCrcCache() { var header = ImUtf8.CollapsingHeader("CRC Cache"u8); if (!header) return; + if (ImUtf8.InputText("##crcInput"u8, ref _crcInput, "Input path for CRC..."u8)) + _crcPath = new FullPath(_crcInput); + + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImUtf8.Text($" CRC32: {_crcPath.InternalName.CiCrc32:X8}"); + ImUtf8.Text($"CI CRC32: {_crcPath.InternalName.Crc32:X8}"); + ImUtf8.Text($" CRC64: {_crcPath.Crc64:X16}"); + using var table = ImUtf8.Table("table"u8, 2); if (!table) return; - using var font = ImRaii.PushFont(UiBuilder.MonoFont); ImUtf8.TableSetupColumn("Hash"u8, ImGuiTableColumnFlags.WidthFixed, 18 * UiBuilder.MonoFont.GetCharAdvance('0')); ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0')); ImGui.TableHeadersRow(); From 688b84141f3ea1964f7f589503d3af516b1531d4 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 20 Nov 2024 09:46:01 +0000 Subject: [PATCH 453/865] [CI] Updating repo.json for testing_1.3.0.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 77f5012e..02437b14 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.0.0", - "TestingAssemblyVersion": "1.3.0.3", + "TestingAssemblyVersion": "1.3.0.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ce75471e5129068ba765cbff8cdad5626866c31d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 20 Nov 2024 18:08:24 +0100 Subject: [PATCH 454/865] Fix issue with resetting GEQP parameters on reload (again?) --- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Meta/Manipulations/MetaDictionary.cs | 11 +++++++++++ Penumbra/Mods/Editor/ModMetaEditor.cs | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1c82c086..c855c17c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1c82c086704e2f1b3608644a9b1d70628fbe0ca9 +Subproject commit c855c17cffd7d270c3f013e01767cd052c24c462 diff --git a/Penumbra.String b/Penumbra.String index bd52d080..dd83f972 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit bd52d080b72d67263dc47068e461f17c93bdc779 +Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936 diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 70d4fd47..da061bec 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -79,6 +79,17 @@ public class MetaDictionary _globalEqp.Clear(); } + public void ClearForDefault() + { + Count = _globalEqp.Count; + _imc.Clear(); + _eqp.Clear(); + _eqdp.Clear(); + _est.Clear(); + _rsp.Clear(); + _gmp.Clear(); + } + public bool Equals(MetaDictionary other) => Count == other.Count && _imc.SetEquals(other._imc) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 64c585ea..217ba93d 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -69,7 +69,8 @@ public class ModMetaEditor( public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { var clone = dict.Clone(); - dict.Clear(); + dict.ClearForDefault(); + var count = 0; foreach (var (key, value) in clone.Imc) { From f2bdaf1b490204de39f668707b3f6d319f5e0eb9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 00:35:17 +0100 Subject: [PATCH 455/865] Circumvent rsf not existing. --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../Hooks/ResourceLoading/TexMdlService.cs | 27 ++++++++++++++++--- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c855c17c..07d18f7f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c855c17cffd7d270c3f013e01767cd052c24c462 +Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index a1dd374f..3deeb107 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -96,6 +96,7 @@ public class HookOverrides public bool CheckFileState; public bool TexResourceHandleOnLoad; public bool LoadMdlFileExtern; + public bool SoundOnLoad; } public struct ResourceHooks diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs index 9c17e0cf..b43f1ed5 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs @@ -7,6 +7,7 @@ using Penumbra.GameData; using Penumbra.Interop.Structs; using Penumbra.String.Classes; using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; +using TextureResourceHandle = Penumbra.Interop.Structs.TextureResourceHandle; namespace Penumbra.Interop.Hooks.ResourceLoading; @@ -52,7 +53,8 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService _loadMdlFileExternHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad) _textureOnLoadHook.Enable(); - _soundOnLoadHook.Enable(); + if (!HookOverrides.Instance.ResourceLoading.SoundOnLoad) + _soundOnLoadHook.Enable(); } /// Add CRC64 if the given file is a model or texture file and has an associated path. @@ -80,6 +82,7 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService /// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes. /// private readonly Dictionary _customFileCrc = []; + public IReadOnlyDictionary CustomCache => _customFileCrc; @@ -98,15 +101,31 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService private delegate byte SoundOnLoadDelegate(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk); - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 8B 79 ?? 48 8B DA 8B D7")] + [Signature(Sigs.LoadScdFileLocal)] private readonly delegate* unmanaged _loadScdFileLocal = null!; - [Signature("40 56 57 41 54 48 81 EC 90 00 00 00 80 3A 0B 45 0F B6 E0 48 8B F2", DetourName = nameof(OnScdLoadDetour))] + [Signature(Sigs.SoundOnLoad, DetourName = nameof(OnScdLoadDetour))] private readonly Hook _soundOnLoadHook = null!; + [Signature(Sigs.RsfServiceAddress, ScanType = ScanType.StaticAddress)] + private readonly nint* _rsfService = null; + private byte OnScdLoadDetour(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk) { - var ret = _soundOnLoadHook.Original(handle, descriptor, unk); + byte ret; + if (*_rsfService == nint.Zero) + { + Penumbra.Log.Debug( + $"Resource load of {handle->FileName} before FFXIV RSF-service was instantiated, workaround by setting pointer."); + *_rsfService = 1; + ret = _soundOnLoadHook.Original(handle, descriptor, unk); + *_rsfService = nint.Zero; + } + else + { + ret = _soundOnLoadHook.Original(handle, descriptor, unk); + } + if (!_scdReturnData.Value) return ret; From ee48ea0166171e5437a5d5731a069aeb6aabc99f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 00:43:36 +0100 Subject: [PATCH 456/865] Some stashed changes already applied. --- Penumbra/Collections/ModCollection.cs | 1 + .../Hooks/ResourceLoading/ResourceLoader.cs | 8 ++++---- .../{TexMdlService.cs => RsfService.cs} | 4 ++-- .../Interop/PathResolving/PathDataHandler.cs | 5 +++++ .../Interop/Processing/ImcFilePostProcessor.cs | 3 ++- Penumbra/UI/ResourceWatcher/Record.cs | 14 +++++++++++--- Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcherTable.cs | 18 +++++++++++++++++- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 15 ++++++++------- Penumbra/UI/Tabs/Debug/DebugTab.cs | 8 ++++---- 10 files changed, 55 insertions(+), 23 deletions(-) rename Penumbra/Interop/Hooks/ResourceLoading/{TexMdlService.cs => RsfService.cs} (96%) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index eb5ab46a..db9c19cb 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -57,6 +57,7 @@ public partial class ModCollection public int ChangeCounter { get; private set; } public uint ImcChangeCounter { get; set; } + public uint AtchChangeCounter { get; set; } /// Increment the number of changes in the effective file list. public int IncrementCounter() diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 442bac15..47f96d98 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -15,18 +15,18 @@ public unsafe class ResourceLoader : IDisposable, IService { private readonly ResourceService _resources; private readonly FileReadService _fileReadService; - private readonly TexMdlScdService _texMdlScdService; + private readonly RsfService _rsfService; private readonly PapHandler _papHandler; private readonly Configuration _config; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlScdService texMdlScdService, Configuration config) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config) { _resources = resources; _fileReadService = fileReadService; - _texMdlScdService = texMdlScdService; + _rsfService = rsfService; _config = config; ResetResolvePath(); @@ -140,7 +140,7 @@ public unsafe class ResourceLoader : IDisposable, IService return; } - _texMdlScdService.AddCrc(type, resolvedPath); + _rsfService.AddCrc(type, resolvedPath); // Replace the hash and path with the correct one for the replacement. hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs similarity index 96% rename from Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs rename to Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs index b43f1ed5..7ac1563f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/TexMdlService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs @@ -11,7 +11,7 @@ using TextureResourceHandle = Penumbra.Interop.Structs.TextureResourceHandle; namespace Penumbra.Interop.Hooks.ResourceLoading; -public unsafe class TexMdlScdService : IDisposable, IRequiredService +public unsafe class RsfService : IDisposable, IRequiredService { /// /// We need to be able to obtain the requested LoD level. @@ -43,7 +43,7 @@ public unsafe class TexMdlScdService : IDisposable, IRequiredService private readonly LodService _lodService; - public TexMdlScdService(IGameInteropProvider interop) + public RsfService(IGameInteropProvider interop) { interop.InitializeFromAttributes(this); _lodService = new LodService(interop); diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 9410ff98..5439151f 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -44,6 +44,11 @@ public static class PathDataHandler public static FullPath CreateAvfx(CiByteString path, ModCollection collection) => CreateBase(path, collection); + /// Create the encoding path for an ATCH file. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static FullPath CreateAtch(CiByteString path, ModCollection collection) + => new($"|{collection.LocalId.Id}_{collection.AtchChangeCounter}_{DiscriminatorString}|{path}"); + /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index 33a3941a..a3233cfb 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -1,3 +1,4 @@ +using Dalamud.Game.ClientState.JobGauge.Types; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -24,7 +25,7 @@ public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFileP return; file.Replace(resource); - Penumbra.Log.Information( + Penumbra.Log.Verbose( $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); } } diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index b69d9944..7338e5a9 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -3,6 +3,7 @@ using Penumbra.Collections; using Penumbra.Enums; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.UI.ResourceWatcher; @@ -24,14 +25,16 @@ internal unsafe struct Record public ModCollection? Collection; public ResourceHandle* Handle; public ResourceTypeFlag ResourceType; - public ResourceCategoryFlag Category; + public ulong Crc64; public uint RefCount; + public ResourceCategoryFlag Category; public RecordType RecordType; public OptionalBool Synchronously; public OptionalBool ReturnValue; public OptionalBool CustomLoad; public LoadState LoadState; + public static Record CreateRequest(CiByteString path, bool sync) => new() { @@ -49,6 +52,7 @@ internal unsafe struct Record CustomLoad = OptionalBool.Null, AssociatedGameObject = string.Empty, LoadState = LoadState.None, + Crc64 = 0, }; public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) @@ -70,15 +74,16 @@ internal unsafe struct Record CustomLoad = false, AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, + Crc64 = 0, }; } - public static Record CreateLoad(CiByteString path, CiByteString originalPath, ResourceHandle* handle, ModCollection collection, + public static Record CreateLoad(FullPath path, CiByteString originalPath, ResourceHandle* handle, ModCollection collection, string associatedGameObject) => new() { Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), + Path = path.InternalName.IsOwned ? path.InternalName : path.InternalName.Clone(), OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), Collection = collection, Handle = handle, @@ -91,6 +96,7 @@ internal unsafe struct Record CustomLoad = true, AssociatedGameObject = associatedGameObject, LoadState = handle->LoadState, + Crc64 = path.Crc64, }; public static Record CreateDestruction(ResourceHandle* handle) @@ -112,6 +118,7 @@ internal unsafe struct Record CustomLoad = OptionalBool.Null, AssociatedGameObject = string.Empty, LoadState = handle->LoadState, + Crc64 = 0, }; } @@ -132,5 +139,6 @@ internal unsafe struct Record CustomLoad = custom, AssociatedGameObject = string.Empty, LoadState = handle->LoadState, + Crc64 = 0, }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 6f1ce9cf..d432e97e 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -250,7 +250,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService var record = manipulatedPath == null ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) - : Record.CreateLoad(manipulatedPath.Value.InternalName, path.Path, handle, data.ModCollection, Name(data)); + : Record.CreateLoad(manipulatedPath.Value, path.Path, handle, data.ModCollection, Name(data)); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 2bb71b87..88b7120d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -29,7 +29,8 @@ internal sealed class ResourceWatcherTable : Table new HandleColumn { Label = "Resource" }, new LoadStateColumn { Label = "State" }, new RefCountColumn { Label = "#Ref" }, - new DateColumn { Label = "Time" } + new DateColumn { Label = "Time" }, + new Crc64Column { Label = "Crc64" } ) { } @@ -144,6 +145,21 @@ internal sealed class ResourceWatcherTable : Table => ImGui.TextUnformatted($"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}"); } + private sealed class Crc64Column : ColumnString + { + public override float Width + => UiBuilder.MonoFont.GetCharAdvance('0') * 17; + + public override unsafe string ToName(Record item) + => item.Crc64 != 0 ? $"{item.Crc64:X16}" : string.Empty; + + public override unsafe void DrawColumn(Record item, int _) + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont, item.Handle != null); + ImUtf8.Text(ToName(item)); + } + } + private sealed class CollectionColumn : ColumnString { diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index 3e407e99..d9058083 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Text; using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; namespace Penumbra.UI.Tabs.Debug; @@ -18,27 +19,27 @@ public static class AtchDrawer ImGui.SameLine(); using (ImUtf8.Group()) { - ImUtf8.Text($"{file.Entries.Count}"); - if (file.Entries.Count == 0) + ImUtf8.Text($"{file.Points.Count}"); + if (file.Points.Count == 0) { ImUtf8.Text("0"u8); return; } - ImUtf8.Text($"{file.Entries[0].States.Count}"); + ImUtf8.Text($"{file.Points[0].Entries.Length}"); } - foreach (var (entry, index) in file.Entries.WithIndex()) + foreach (var (entry, index) in file.Points.WithIndex()) { using var id = ImUtf8.PushId(index); - using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Name.Span}"); + using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Type.ToName()}"); if (tree) { ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - foreach (var (state, i) in entry.States.WithIndex()) + foreach (var (state, i) in entry.Entries.WithIndex()) { id.Push(i); - using var t = ImUtf8.TreeNode(state.Bone.Span); + using var t = ImUtf8.TreeNode(state.Bone); if (t) { ImUtf8.TreeNode($"Scale: {state.Scale}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 28911b05..fc735d04 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -104,7 +104,7 @@ public class DebugTab : Window, ITab, IUiService private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; private readonly HookOverrideDrawer _hookOverrides; - private readonly TexMdlScdService _texMdlScdService; + private readonly RsfService _rsfService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -115,7 +115,7 @@ public class DebugTab : Window, ITab, IUiService CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, TexMdlScdService texMdlScdService) + HookOverrideDrawer hookOverrides, RsfService rsfService) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -153,7 +153,7 @@ public class DebugTab : Window, ITab, IUiService _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; _hookOverrides = hookOverrides; - _texMdlScdService = texMdlScdService; + _rsfService = rsfService; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -1087,7 +1087,7 @@ public class DebugTab : Window, ITab, IUiService ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0')); ImGui.TableHeadersRow(); - foreach (var (hash, type) in _texMdlScdService.CustomCache) + foreach (var (hash, type) in _rsfService.CustomCache) { ImGui.TableNextColumn(); ImUtf8.Text($"{hash:X16}"); From 37332c432b4128008538058e2088ed305117f26c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 12:34:45 +0100 Subject: [PATCH 457/865] 1.3.1.0 --- Penumbra/Penumbra.cs | 2 +- Penumbra/UI/Changelog.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 41d8f668..2c70816e 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", + "IllusioVitae", "Aetherment", "LoporritSync", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 48ac90d8..0b0ca81a 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -54,10 +54,28 @@ public class PenumbraChangelog : IUiService Add1_1_1_0(Changelog); Add1_2_1_0(Changelog); Add1_3_0_0(Changelog); + Add1_3_1_0(Changelog); } #region Changelogs + private static void Add1_3_1_0(Changelog log) + => log.NextVersion("Version 1.3.1.0") + .RegisterEntry("Penumbra has been updated for Dalamud API 11 and patch 7.1.") + .RegisterImportant("There are some known issues with potential crashes using certain VFX/SFX mods, probably related to sound files.") + .RegisterEntry("If you encounter those issues, please report them in the discord and potentially disable the corresponding mods for the time being.", 1) + .RegisterImportant("The modding of .atch files has been disabled. Outdated modded versions of these files cause crashes when loaded.") + .RegisterEntry("A better way for modular modding of .atch files via meta changes will release to the testing branch soonish.", 1) + .RegisterHighlight("Temporary collections (as created by Mare) will now always respect ownership.") + .RegisterEntry("This means that you can toggle this setting off if you do not want it, and Mare will still work for minions and mounts of other players.", 1) + .RegisterEntry("The new physics and animation engine files (.kdb and .bnmb) should now be correctly redirected and respect EST changes.") + .RegisterEntry("Fixed issues with EQP entries being labeled wrongly and global EQP not changing all required values for earrings.") + .RegisterEntry("Fixed an issue with global EQP changes of a mod being reset upon reloading the mod.") + .RegisterEntry("Fixed another issue with left rings and mare synchronization / the on-screen tab.") + .RegisterEntry("Maybe fixed some issues with characters appearing in the login screen being misidentified.") + .RegisterEntry("Some improvements for debug visualization have been made."); + + private static void Add1_3_0_0(Changelog log) => log.NextVersion("Version 1.3.0.0") From 977cb2196a138ed6cb6f4a14d939ea1272ff72b8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Nov 2024 13:29:57 +0000 Subject: [PATCH 458/865] [CI] Updating repo.json for 1.3.1.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 02437b14..e7ad4df3 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.3.0.0", - "TestingAssemblyVersion": "1.3.0.4", + "AssemblyVersion": "1.3.1.0", + "TestingAssemblyVersion": "1.3.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 10, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 234130cf862ee821e9dd6e0eb9470d68adb38a06 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:44:00 +0100 Subject: [PATCH 459/865] Update repo.json --- repo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repo.json b/repo.json index e7ad4df3..659f4c24 100644 --- a/repo.json +++ b/repo.json @@ -9,7 +9,7 @@ "TestingAssemblyVersion": "1.3.1.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 10, + "DalamudApiLevel": 11, "TestingDalamudApiLevel": 11, "IsHide": "False", "IsTestingExclusive": "False", From 06ba0ba956b0d4d121e5949e9b7c69c56fe3fa0c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 17:31:53 +0100 Subject: [PATCH 460/865] Fix glasses issue with resource trees. --- Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b1cbb74d..e67bf913 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -53,7 +53,7 @@ internal partial record ResolveContext if (characterRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - var accessory = slotIndex >= 5; + var accessory = IsEquipmentSlot(slotIndex); if ((ushort)characterRaceCode % 10 != 1 && accessory) return GenderRace.MidlanderMale; From 22be9f2d0726f78dd335b96fd84e4000bb8972c6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Nov 2024 16:34:25 +0000 Subject: [PATCH 461/865] [CI] Updating repo.json for 1.3.1.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 659f4c24..f1734ed7 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.3.1.0", - "TestingAssemblyVersion": "1.3.1.0", + "AssemblyVersion": "1.3.1.1", + "TestingAssemblyVersion": "1.3.1.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 17d8826ae920b1509cef8b9612fdaf5cef93f17a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 19:12:53 +0100 Subject: [PATCH 462/865] This time correctly, maybe? --- Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index e67bf913..0c36b745 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -53,7 +53,7 @@ internal partial record ResolveContext if (characterRaceCode == GenderRace.MidlanderMale) return GenderRace.MidlanderMale; - var accessory = IsEquipmentSlot(slotIndex); + var accessory = !IsEquipmentSlot(slotIndex); if ((ushort)characterRaceCode % 10 != 1 && accessory) return GenderRace.MidlanderMale; From 5a46361d4f478eeecc45cc82fdde609e7aa92e98 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 22 Nov 2024 18:16:51 +0000 Subject: [PATCH 463/865] [CI] Updating repo.json for 1.3.1.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f1734ed7..a714fbe0 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.3.1.1", - "TestingAssemblyVersion": "1.3.1.1", + "AssemblyVersion": "1.3.1.2", + "TestingAssemblyVersion": "1.3.1.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 25aac1a03e2b197f32f493835393c809654fbaf2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 23 Nov 2024 13:57:54 +0100 Subject: [PATCH 464/865] Fix CalculateHeight. --- Penumbra/Interop/Hooks/Meta/CalculateHeight.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs index 0e85b3ae..3dac17bd 100644 --- a/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs +++ b/Penumbra/Interop/Hooks/Meta/CalculateHeight.cs @@ -14,19 +14,19 @@ public sealed unsafe class CalculateHeight : FastHook { _collectionResolver = collectionResolver; _metaState = metaState; - Task = hooks.CreateHook("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, + Task = hooks.CreateHook("Calculate Height", (nint)ModelContainer.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight); } - public delegate ulong Delegate(Character* character); + public delegate float Delegate(ModelContainer* character); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private ulong Detour(Character* character) + private float Detour(ModelContainer* container) { - var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true); + var collection = _collectionResolver.IdentifyCollection((GameObject*)container->OwnerObject, true); _metaState.RspCollection.Push(collection); - var ret = Task.Result.Original.Invoke(character); - Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}."); + var ret = Task.Result.Original.Invoke(container); + Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)container:X} -> {ret}."); _metaState.RspCollection.Pop(); return ret; } From 9822ab4128dedc12d9c5cd08af502dce3f06bb53 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Nov 2024 16:59:07 +0100 Subject: [PATCH 465/865] Add some debug helper output for SeFileDescriptor. --- Penumbra/Interop/Structs/SeFileDescriptor.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Penumbra/Interop/Structs/SeFileDescriptor.cs b/Penumbra/Interop/Structs/SeFileDescriptor.cs index 67730799..02ab4dc8 100644 --- a/Penumbra/Interop/Structs/SeFileDescriptor.cs +++ b/Penumbra/Interop/Structs/SeFileDescriptor.cs @@ -1,3 +1,6 @@ +using Dalamud.Memory; +using Penumbra.String.Functions; + namespace Penumbra.Interop.Structs; [StructLayout(LayoutKind.Explicit)] @@ -14,4 +17,18 @@ public unsafe struct SeFileDescriptor [FieldOffset(0x70)] public char Utf16FileName; + + public FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle* CsResourceHandele + => (FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle*)ResourceHandle; + + public string FileName + { + get + { + fixed (char* ptr = &Utf16FileName) + { + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr).ToString(); + } + } + } } From d0e0ae46e67fc7e5c5a7af663811521cb1080c5a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 25 Nov 2024 17:46:14 +0100 Subject: [PATCH 466/865] Push an ID in itemselector. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 95b8d177..215e0172 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 95b8d177883b03f804d77434f45e9de97fdb9adf +Subproject commit 215e01722a319c70b271dd23a40d99edc3fc197e From d2a015f32ad859b703449fc025546a30bed7156f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Nov 2024 01:58:44 +0100 Subject: [PATCH 467/865] Ughhhhhhhhhh --- Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs | 1 - Penumbra/Penumbra.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs index 7ac1563f..e7f06f91 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/RsfService.cs @@ -132,7 +132,6 @@ public unsafe class RsfService : IDisposable, IRequiredService // Function failed on a replaced scd, call local. _scdReturnData.Value = false; ret = _loadScdFileLocal(handle, descriptor, unk); - _updateCategory((TextureResourceHandle*)handle); return ret; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 2c70816e..1bf8844c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From cc49bdcb3669452debf57879d9ec4a2c78cfbd94 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 26 Nov 2024 01:02:41 +0000 Subject: [PATCH 468/865] [CI] Updating repo.json for 1.3.1.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index a714fbe0..f5a8e2f1 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.3.1.2", - "TestingAssemblyVersion": "1.3.1.2", + "AssemblyVersion": "1.3.1.3", + "TestingAssemblyVersion": "1.3.1.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 65538868c314557be65cf0e2b2e2231de1b15f45 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Nov 2024 15:49:33 +0100 Subject: [PATCH 469/865] Add Artemis --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 1bf8844c..917dba6c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -186,7 +186,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From b1be868a6a9eaa94fec3f307cd0223e0c6b38e3c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 22 Nov 2024 12:19:14 +0100 Subject: [PATCH 470/865] Atch stuff. --- Penumbra.GameData | 2 +- Penumbra/Api/Api/MetaApi.cs | 12 + Penumbra/Collections/Cache/AtchCache.cs | 122 +++++++++ Penumbra/Collections/Cache/CollectionCache.cs | 33 ++- Penumbra/Collections/Cache/MetaCache.cs | 10 +- Penumbra/Interop/Hooks/DebugHook.cs | 10 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../Interop/Hooks/Meta/AtchCallerHook1.cs | 39 +++ .../Interop/Hooks/Meta/AtchCallerHook2.cs | 38 +++ Penumbra/Interop/PathResolving/MetaState.cs | 1 + .../Interop/PathResolving/PathResolver.cs | 9 +- .../Processing/AtchFilePostProcessor.cs | 43 +++ .../Processing/AtchPathPreProcessor.cs | 44 ++++ .../Processing/GamePathPreProcessService.cs | 3 +- Penumbra/Meta/AtchManager.cs | 26 ++ Penumbra/Meta/Manipulations/AtchIdentifier.cs | 77 ++++++ .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 72 ++++- Penumbra/Meta/MetaFileManager.cs | 5 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 1 + Penumbra/Penumbra.cs | 1 + .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 245 ++++++++++++++++++ .../UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/EqpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/EstMetaDrawer.cs | 3 +- .../Meta/GlobalEqpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/GmpMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/Meta/ImcMetaDrawer.cs | 9 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 14 +- .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 7 +- .../UI/AdvancedWindow/Meta/RspMetaDrawer.cs | 3 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + 32 files changed, 802 insertions(+), 43 deletions(-) create mode 100644 Penumbra/Collections/Cache/AtchCache.cs create mode 100644 Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs create mode 100644 Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs create mode 100644 Penumbra/Interop/Processing/AtchFilePostProcessor.cs create mode 100644 Penumbra/Interop/Processing/AtchPathPreProcessor.cs create mode 100644 Penumbra/Meta/AtchManager.cs create mode 100644 Penumbra/Meta/Manipulations/AtchIdentifier.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 07d18f7f..2b0c7f3b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 07d18f7f7218811956e6663592e53c4145f2d862 +Subproject commit 2b0c7f3bee0bc2eb466540d2fac265804354493d diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 6f3ed51e..217cb1e3 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -5,6 +5,7 @@ using OtterGui; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Files.Utility; using Penumbra.GameData.Structs; using Penumbra.Interop.PathResolving; @@ -66,6 +67,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver MetaDictionary.SerializeTo(array, cache.Est.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } return Functions.ToCompressedBase64(array, 0); @@ -97,6 +99,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver WriteCache(zipStream, cache.Est); WriteCache(zipStream, cache.Rsp); WriteCache(zipStream, cache.Gmp); + WriteCache(zipStream, cache.Atch); cache.GlobalEqp.EnterReadLock(); try @@ -246,6 +249,15 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } + var atchCount = r.ReadInt32(); + for (var i = 0; i < atchCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + var globalEqpCount = r.ReadInt32(); for (var i = 0; i < globalEqpCount; ++i) { diff --git a/Penumbra/Collections/Cache/AtchCache.cs b/Penumbra/Collections/Cache/AtchCache.cs new file mode 100644 index 00000000..9e0f6caf --- /dev/null +++ b/Penumbra/Collections/Cache/AtchCache.cs @@ -0,0 +1,122 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class AtchCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + private readonly Dictionary)> _atchFiles = []; + + public bool HasFile(GenderRace gr) + => _atchFiles.ContainsKey(gr); + + public bool GetFile(GenderRace gr, [NotNullWhen(true)] out AtchFile? file) + { + if (!_atchFiles.TryGetValue(gr, out var p)) + { + file = null; + return false; + } + + file = p.Item1; + return true; + } + + public void Reset() + { + foreach (var (_, (_, set)) in _atchFiles) + set.Clear(); + + _atchFiles.Clear(); + Clear(); + } + + protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry) + { + ++Collection.AtchChangeCounter; + ApplyFile(identifier, entry); + } + + private void ApplyFile(AtchIdentifier identifier, AtchEntry entry) + { + try + { + if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) + { + if (!Manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile)) + throw new Exception($"Invalid Atch File for {identifier.GenderRace.ToName()} requested."); + + pair = (baseFile.Clone(), []); + } + + + if (!Apply(pair.Item1, identifier, entry)) + return; + + pair.Item2.Add(identifier); + _atchFiles[identifier.GenderRace] = pair; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not apply ATCH Manipulation {identifier}:\n{e}"); + } + } + + protected override void RevertModInternal(AtchIdentifier identifier) + { + ++Collection.AtchChangeCounter; + if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) + return; + + if (!pair.Item2.Remove(identifier)) + return; + + if (pair.Item2.Count == 0) + { + _atchFiles.Remove(identifier.GenderRace); + return; + } + + var def = GetDefault(Manager, identifier); + if (def == null) + throw new Exception($"Reverting an .atch mod had no default value for the identifier to revert to."); + + Apply(pair.Item1, identifier, def.Value); + } + + public static AtchEntry? GetDefault(MetaFileManager manager, AtchIdentifier identifier) + { + if (!manager.AtchManager.AtchFileBase.TryGetValue(identifier.GenderRace, out var baseFile)) + return null; + + if (baseFile.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point) + return null; + + if (point.Entries.Length <= identifier.EntryIndex) + return null; + + return point.Entries[identifier.EntryIndex]; + } + + public static bool Apply(AtchFile file, AtchIdentifier identifier, in AtchEntry entry) + { + if (file.Points.FirstOrDefault(p => p.Type == identifier.Type) is not { } point) + return false; + + if (point.Entries.Length <= identifier.EntryIndex) + return false; + + point.Entries[identifier.EntryIndex] = entry; + return true; + } + + protected override void Dispose(bool _) + { + Clear(); + _atchFiles.Clear(); + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index abc0dff8..64cf54ea 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -228,20 +228,25 @@ public sealed class CollectionCache : IDisposable foreach (var (path, file) in files.FileRedirections) AddFile(path, file, mod); - foreach (var (identifier, entry) in files.Manipulations.Eqp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Eqdp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Est) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Gmp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Rsp) - AddManipulation(mod, identifier, entry); - foreach (var (identifier, entry) in files.Manipulations.Imc) - AddManipulation(mod, identifier, entry); - foreach (var identifier in files.Manipulations.GlobalEqp) - AddManipulation(mod, identifier, null!); + if (files.Manipulations.Count > 0) + { + foreach (var (identifier, entry) in files.Manipulations.Eqp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Eqdp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Est) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Gmp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Rsp) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Imc) + AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Atch) + AddManipulation(mod, identifier, entry); + foreach (var identifier in files.Manipulations.GlobalEqp) + AddManipulation(mod, identifier, null!); + } if (addMetaChanges) { diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 05a94ac5..7d8586c3 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -1,4 +1,5 @@ using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; @@ -14,11 +15,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly GmpCache Gmp = new(manager, collection); public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); + public readonly AtchCache Atch = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -27,6 +29,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Gmp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -37,6 +40,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Gmp.Reset(); Rsp.Reset(); Imc.Reset(); + Atch.Reset(); GlobalEqp.Clear(); } @@ -52,6 +56,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Gmp.Dispose(); Rsp.Dispose(); Imc.Dispose(); + Atch.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -65,6 +70,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i => Gmp.TryGetValue(i, out var p) && Convert(p, out mod), ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod), RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), + AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -85,6 +91,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i => Gmp.RevertMod(i, out mod), ImcIdentifier i => Imc.RevertMod(i, out mod), RspIdentifier i => Rsp.RevertMod(i, out mod), + AtchIdentifier i => Atch.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -100,6 +107,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) GmpIdentifier i when entry is GmpEntry e => Gmp.ApplyMod(mod, i, e), ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e), RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), + AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Interop/Hooks/DebugHook.cs b/Penumbra/Interop/Hooks/DebugHook.cs index db14805c..fe9754f9 100644 --- a/Penumbra/Interop/Hooks/DebugHook.cs +++ b/Penumbra/Interop/Hooks/DebugHook.cs @@ -1,5 +1,6 @@ using Dalamud.Hooking; using OtterGui.Services; +using Penumbra.Interop.Structs; namespace Penumbra.Interop.Hooks; @@ -31,12 +32,13 @@ public sealed unsafe class DebugHook : IHookService public bool Finished => _task?.IsCompletedSuccessfully ?? true; - private delegate void Delegate(nint a, int b, nint c, float* d); + private delegate nint Delegate(ResourceHandle* a, int b, int c); - private void Detour(nint a, int b, nint c, float* d) + private nint Detour(ResourceHandle* a, int b, int c) { - _task!.Result.Original(a, b, c, d); - Penumbra.Log.Information($"[Debug Hook] Results with 0x{a:X} {b} {c:X} {d[0]} {d[1]} {d[2]} {d[3]}."); + var ret = _task!.Result.Original(a, b, c); + Penumbra.Log.Information($"[Debug Hook] Results with 0x{(nint)a:X}, {b}, {c} -> 0x{ret:X}."); + return ret; } } #endif diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 3deeb107..63d93c9d 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -62,6 +62,8 @@ public class HookOverrides public bool SetupVisor; public bool UpdateModel; public bool UpdateRender; + public bool AtchCaller1; + public bool AtchCaller2; } public struct ObjectHooks diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs new file mode 100644 index 00000000..748ca93a --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -0,0 +1,39 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class AtchCallerHook1 : FastHook, IDisposable +{ + public delegate void Delegate(DrawObjectData* data, uint slot, nint unk, Model playerModel); + + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public AtchCallerHook1(HookManager hooks, MetaState metaState, CollectionResolver collectionResolver) + { + _metaState = metaState; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("AtchCaller1", Sigs.AtchCaller1, Detour, + metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller1); + if (!HookOverrides.Instance.Meta.AtchCaller1) + _metaState.Config.ModsEnabled += Toggle; + } + + private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel) + { + var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + _metaState.AtchCollection.Push(collection); + Task.Result.Original(data, slot, unk, playerModel); + _metaState.AtchCollection.Pop(); + Penumbra.Log.Excessive( + $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); + } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; +} diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs new file mode 100644 index 00000000..9b3349f2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -0,0 +1,38 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Meta; + +public unsafe class AtchCallerHook2 : FastHook, IDisposable +{ + public delegate void Delegate(DrawObjectData* data, uint slot, nint unk, Model playerModel, uint unk2); + + private readonly CollectionResolver _collectionResolver; + private readonly MetaState _metaState; + + public AtchCallerHook2(HookManager hooks, MetaState metaState, CollectionResolver collectionResolver) + { + _metaState = metaState; + _collectionResolver = collectionResolver; + Task = hooks.CreateHook("AtchCaller2", Sigs.AtchCaller2, Detour, + metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller2); + if (!HookOverrides.Instance.Meta.AtchCaller2) + _metaState.Config.ModsEnabled += Toggle; + } + + private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel, uint unk2) + { + var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + _metaState.AtchCollection.Push(collection); + Task.Result.Original(data, slot, unk, playerModel, unk2); + _metaState.AtchCollection.Pop(); + Penumbra.Log.Excessive( + $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.AnonymizedName}."); + } + + public void Dispose() + => _metaState.Config.ModsEnabled -= Toggle; +} diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index e709c210..e7fc3176 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -49,6 +49,7 @@ public sealed unsafe class MetaState : IDisposable, IService public readonly Stack EqdpCollection = []; public readonly Stack EstCollection = []; public readonly Stack RspCollection = []; + public readonly Stack AtchCollection = []; public readonly Stack<(ResolveData Collection, PrimaryId Id)> GmpCollection = []; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index a7af42e3..0b6c8340 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,3 +1,4 @@ +using System.Linq; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -54,7 +55,7 @@ public class PathResolver : IDisposable, IService // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. if (resourceType is ResourceType.Atch) - return (null, ResolveData.Invalid); + return ResolveAtch(path); return category switch { @@ -142,4 +143,10 @@ public class PathResolver : IDisposable, IService private (FullPath?, ResolveData) ResolveUi(Utf8GamePath path) => (_collectionManager.Active.Interface.ResolvePath(path), _collectionManager.Active.Interface.ToResolveData()); + + public (FullPath?, ResolveData) ResolveAtch(Utf8GamePath gamePath) + { + _metaState.AtchCollection.TryPeek(out var resolveData); + return _preprocessor.PreProcess(resolveData, gamePath.Path, false, ResourceType.Atch, null, gamePath); + } } diff --git a/Penumbra/Interop/Processing/AtchFilePostProcessor.cs b/Penumbra/Interop/Processing/AtchFilePostProcessor.cs new file mode 100644 index 00000000..e4fab022 --- /dev/null +++ b/Penumbra/Interop/Processing/AtchFilePostProcessor.cs @@ -0,0 +1,43 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; +using Penumbra.String; + +namespace Penumbra.Interop.Processing; + +public sealed class AtchFilePostProcessor(CollectionStorage collections, XivFileAllocator allocator) + : IFilePostProcessor +{ + private readonly IFileAllocator _allocator = allocator; + + public ResourceType Type + => ResourceType.Atch; + + public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan additionalData) + { + if (!PathDataHandler.Read(additionalData, out var data) || data.Discriminator != PathDataHandler.Discriminator) + return; + + var collection = collections.ByLocalId(data.Collection); + if (collection.MetaCache is not { } cache) + return; + + if (!AtchPathPreProcessor.TryGetAtchGenderRace(originalGamePath, out var gr)) + return; + + if (!collection.MetaCache.Atch.GetFile(gr, out var file)) + return; + + using var bytes = file.Write(); + var length = (int)bytes.Position; + var alloc = _allocator.Allocate(length, 1); + bytes.GetBuffer().AsSpan(0, length).CopyTo(new Span(alloc, length)); + var (oldData, oldLength) = resource->GetData(); + _allocator.Release((void*)oldData, oldLength); + resource->SetData((nint)alloc, length); + Penumbra.Log.Information($"Post-Processed {originalGamePath} on resource 0x{(nint)resource:X} with {collection} for {gr.ToName()}."); + } +} diff --git a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs new file mode 100644 index 00000000..9a9096f3 --- /dev/null +++ b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs @@ -0,0 +1,44 @@ +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.Interop.PathResolving; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Processing; + +public sealed class AtchPathPreProcessor : IPathPreProcessor +{ + public ResourceType Type + => ResourceType.Atch; + + public FullPath? PreProcess(ResolveData resolveData, CiByteString path, Utf8GamePath _, bool nonDefault, FullPath? resolved) + { + if (!resolveData.Valid) + return resolved; + + if (!TryGetAtchGenderRace(path, out var gr)) + return resolved; + + Penumbra.Log.Information($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); + if (resolveData.ModCollection.MetaCache?.Atch.GetFile(gr, out var file) == true) + return PathDataHandler.CreateAtch(path, resolveData.ModCollection); + + return resolved; + } + + public static bool TryGetAtchGenderRace(CiByteString originalGamePath, out GenderRace genderRace) + { + if (originalGamePath[^6] != '1' + || originalGamePath[^7] != '0' + || !ushort.TryParse(originalGamePath.Span[^9..^7], out var grInt) + || grInt > 18) + { + genderRace = GenderRace.Unknown; + return false; + } + + genderRace = (GenderRace)(grInt * 100 + 1); + return true; + } +} diff --git a/Penumbra/Interop/Processing/GamePathPreProcessService.cs b/Penumbra/Interop/Processing/GamePathPreProcessService.cs index 65608ba0..875eb254 100644 --- a/Penumbra/Interop/Processing/GamePathPreProcessService.cs +++ b/Penumbra/Interop/Processing/GamePathPreProcessService.cs @@ -25,8 +25,7 @@ public class GamePathPreProcessService : IService public (FullPath? Path, ResolveData Data) PreProcess(ResolveData resolveData, CiByteString path, bool nonDefault, ResourceType type, - FullPath? resolved, - Utf8GamePath originalPath) + FullPath? resolved, Utf8GamePath originalPath) { if (!_processors.TryGetValue(type, out var processor)) return (resolved, resolveData); diff --git a/Penumbra/Meta/AtchManager.cs b/Penumbra/Meta/AtchManager.cs new file mode 100644 index 00000000..68f2f815 --- /dev/null +++ b/Penumbra/Meta/AtchManager.cs @@ -0,0 +1,26 @@ +using System.Collections.Frozen; +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; + +namespace Penumbra.Interop.Hooks.Meta; + +public sealed unsafe class AtchManager : IService +{ + private static readonly IReadOnlyList GenderRaces = + [ + GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale, GenderRace.ElezenMale, + GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale, GenderRace.RoegadynFemale, + GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale, GenderRace.HrothgarMale, + GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale, + ]; + + public readonly IReadOnlyDictionary AtchFileBase; + + public AtchManager(IDataManager manager) + { + AtchFileBase = GenderRaces.ToFrozenDictionary(gr => gr, + gr => new AtchFile(manager.GetFile($"chara/xls/attachOffset/c{gr.ToRaceCode()}.atch")!.DataSpan)); + } +} diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs new file mode 100644 index 00000000..bce37620 --- /dev/null +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -0,0 +1,77 @@ +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRace, ushort EntryIndex) + : IComparable, IMetaIdentifier +{ + public Gender Gender + => GenderRace.Split().Item1; + + public ModelRace Race + => GenderRace.Split().Item2; + + public int CompareTo(AtchIdentifier other) + { + var typeComparison = Type.CompareTo(other.Type); + if (typeComparison != 0) + return typeComparison; + + var genderRaceComparison = GenderRace.CompareTo(other.GenderRace); + if (genderRaceComparison != 0) + return genderRaceComparison; + + return EntryIndex.CompareTo(other.EntryIndex); + } + + public override string ToString() + => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing specific + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + var race = (int)GenderRace / 100; + var remainder = (int)GenderRace - 100 * race; + if (remainder != 1) + return false; + + return race is >= 0 and <= 18; + } + + public JObject AddToJson(JObject jObj) + { + var (gender, race) = GenderRace.Split(); + jObj["Gender"] = gender.ToString(); + jObj["Race"] = race.ToString(); + jObj["Type"] = Type.ToAbbreviation(); + jObj["Index"] = EntryIndex; + return jObj; + } + + public static AtchIdentifier? FromJson(JObject jObj) + { + var gender = jObj["Gender"]?.ToObject() ?? Gender.Unknown; + var race = jObj["Race"]?.ToObject() ?? ModelRace.Unknown; + var type = AtchExtensions.FromString(jObj["Type"]?.ToObject() ?? string.Empty); + var entryIndex = jObj["Index"]?.ToObject() ?? ushort.MaxValue; + if (entryIndex == ushort.MaxValue || type is AtchType.Unknown) + return null; + + var ret = new AtchIdentifier(type, Names.CombinedRace(gender, race), entryIndex); + return ret.Validate() ? ret : null; + } + + MetaManipulationType IMetaIdentifier.Type + => MetaManipulationType.Atch; +} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index d1668a4d..999fd906 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -14,6 +14,7 @@ public enum MetaManipulationType : byte Gmp = 5, Rsp = 6, GlobalEqp = 7, + Atch = 8, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index da061bec..ca45c777 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Collections.Cache; +using Penumbra.GameData.Files.AtchStructs; using Penumbra.GameData.Structs; using Penumbra.Util; using ImcEntry = Penumbra.GameData.Structs.ImcEntry; @@ -16,6 +17,7 @@ public class MetaDictionary private readonly Dictionary _est = []; private readonly Dictionary _rsp = []; private readonly Dictionary _gmp = []; + private readonly Dictionary _atch = []; private readonly HashSet _globalEqp = []; public IReadOnlyDictionary Imc @@ -36,6 +38,9 @@ public class MetaDictionary public IReadOnlyDictionary Rsp => _rsp; + public IReadOnlyDictionary Atch + => _atch; + public IReadOnlySet GlobalEqp => _globalEqp; @@ -50,6 +55,7 @@ public class MetaDictionary MetaManipulationType.Est => _est.Count, MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Rsp => _rsp.Count, + MetaManipulationType.Atch => _atch.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count, _ => 0, }; @@ -63,6 +69,7 @@ public class MetaDictionary GlobalEqpManipulation i => _globalEqp.Contains(i), GmpIdentifier i => _gmp.ContainsKey(i), ImcIdentifier i => _imc.ContainsKey(i), + AtchIdentifier i => _atch.ContainsKey(i), RspIdentifier i => _rsp.ContainsKey(i), _ => false, }; @@ -76,6 +83,7 @@ public class MetaDictionary _est.Clear(); _rsp.Clear(); _gmp.Clear(); + _atch.Clear(); _globalEqp.Clear(); } @@ -88,6 +96,7 @@ public class MetaDictionary _est.Clear(); _rsp.Clear(); _gmp.Clear(); + _atch.Clear(); } public bool Equals(MetaDictionary other) @@ -98,6 +107,7 @@ public class MetaDictionary && _est.SetEquals(other._est) && _rsp.SetEquals(other._rsp) && _gmp.SetEquals(other._gmp) + && _atch.SetEquals(other._atch) && _globalEqp.SetEquals(other._globalEqp); public IEnumerable Identifiers @@ -107,6 +117,7 @@ public class MetaDictionary .Concat(_est.Keys.Cast()) .Concat(_gmp.Keys.Cast()) .Concat(_rsp.Keys.Cast()) + .Concat(_atch.Keys.Cast()) .Concat(_globalEqp.Cast()); #region TryAdd @@ -171,6 +182,15 @@ public class MetaDictionary return true; } + public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry) + { + if (!_atch.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { if (!_globalEqp.Add(identifier)) @@ -244,6 +264,15 @@ public class MetaDictionary return true; } + public bool Update(AtchIdentifier identifier, in AtchEntry entry) + { + if (!_atch.ContainsKey(identifier)) + return false; + + _atch[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -266,6 +295,9 @@ public class MetaDictionary public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) => _imc.TryGetValue(identifier, out value); + public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) + => _atch.TryGetValue(identifier, out value); + #endregion public bool Remove(IMetaIdentifier identifier) @@ -279,6 +311,7 @@ public class MetaDictionary GmpIdentifier i => _gmp.Remove(i), ImcIdentifier i => _imc.Remove(i), RspIdentifier i => _rsp.Remove(i), + AtchIdentifier i => _atch.Remove(i), _ => false, }; if (ret) @@ -308,6 +341,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._est) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._atch) + TryAdd(identifier, entry); + foreach (var identifier in manips._globalEqp) TryAdd(identifier); } @@ -351,6 +387,12 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; @@ -369,8 +411,9 @@ public class MetaDictionary _est.SetTo(other._est); _rsp.SetTo(other._rsp); _gmp.SetTo(other._gmp); + _atch.SetTo(other._atch); _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; } public void UpdateTo(MetaDictionary other) @@ -381,8 +424,9 @@ public class MetaDictionary _est.UpdateTo(other._est); _rsp.UpdateTo(other._rsp); _gmp.UpdateTo(other._gmp); + _atch.UpdateTo(other._atch); _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; } #endregion @@ -460,6 +504,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(AtchIdentifier identifier, AtchEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Atch.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.ToJson(), + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -487,6 +541,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(ImcIdentifier) && typeof(TEntry) == typeof(ImcEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -531,6 +587,7 @@ public class MetaDictionary SerializeTo(array, value._est); SerializeTo(array, value._rsp); SerializeTo(array, value._gmp); + SerializeTo(array, value._atch); SerializeTo(array, value._globalEqp); array.WriteTo(writer); } @@ -618,6 +675,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid RSP Manipulation encountered."); break; } + case MetaManipulationType.Atch: + { + var identifier = AtchIdentifier.FromJson(manip); + var entry = AtchEntry.FromJson(manip["Entry"] as JObject); + if (identifier.HasValue && entry.HasValue) + dict.TryAdd(identifier.Value, entry.Value); + else + Penumbra.Log.Warning("Invalid ATCH Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); @@ -648,6 +715,7 @@ public class MetaDictionary _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); Count = cache.Count; } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 3755afa2..5250273b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -5,6 +5,7 @@ using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Data; using Penumbra.Import; +using Penumbra.Interop.Hooks.Meta; using Penumbra.Interop.Services; using Penumbra.Meta.Files; using Penumbra.Mods; @@ -25,13 +26,14 @@ public class MetaFileManager : IService internal readonly ObjectIdentification Identifier; internal readonly FileCompactor Compactor; internal readonly ImcChecker ImcChecker; + internal readonly AtchManager AtchManager; internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); internal readonly IFileAllocator XivAllocator; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, - FileCompactor compactor, IGameInteropProvider interop) + FileCompactor compactor, IGameInteropProvider interop, AtchManager atchManager) { CharacterUtility = characterUtility; ResidentResources = residentResources; @@ -41,6 +43,7 @@ public class MetaFileManager : IService ValidityChecker = validityChecker; Identifier = identifier; Compactor = compactor; + AtchManager = atchManager; ImcChecker = new ImcChecker(this); XivAllocator = new XivFileAllocator(interop); interop.InitializeFromAttributes(this); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 217ba93d..7a5142dc 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -58,6 +58,7 @@ public class ModMetaEditor( OtherData[MetaManipulationType.Gmp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Gmp)); OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); + OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 917dba6c..534911df 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,6 +21,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData.Data; +using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.ResourceLoading; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs new file mode 100644 index 00000000..4cf01faa --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -0,0 +1,245 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.AtchStructs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class AtchMetaDrawer : MetaDrawer, IService +{ + public override ReadOnlySpan Label + => "Attachment Points (ATCH)###ATCH"u8; + + public override int NumColumns + => 10; + + public override float ColumnHeight + => 2 * ImUtf8.FrameHeightSpacing; + + private AtchFile? _currentBaseAtchFile; + private AtchPoint? _currentBaseAtchPoint; + private AtchPointCombo _combo; + + public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : base(editor, metaFiles) + { + _combo = new AtchPointCombo(() => _currentBaseAtchFile?.Points.Select(p => p.Type).ToList() ?? []); + } + + private sealed class AtchPointCombo(Func> generator) + : FilterComboCache(generator, MouseWheelType.Control, Penumbra.Log) + { + protected override string ToString(AtchType obj) + => obj.ToName(); + } + + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current ATCH manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atch))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier); + var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, Entry); + + if (DrawIdentifierInput(ref Identifier)) + UpdateEntry(); + + var defaultEntry = AtchCache.GetDefault(MetaFiles, Identifier) ?? default; + DrawEntry(defaultEntry, ref defaultEntry, true); + } + + private void UpdateEntry() + => Entry = _currentBaseAtchPoint!.Entries[Identifier.EntryIndex]; + + protected override void Initialize() + { + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[GenderRace.MidlanderMale]; + _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); + Identifier = new AtchIdentifier(_currentBaseAtchPoint.Type, GenderRace.MidlanderMale, 0); + Entry = _currentBaseAtchPoint.Entries[0]; + } + + protected override void DrawEntry(AtchIdentifier identifier, AtchEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + var defaultEntry = AtchCache.GetDefault(MetaFiles, identifier) ?? default; + if (DrawEntry(defaultEntry, ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(AtchIdentifier, AtchEntry)> Enumerate() + => Editor.Atch.Select(kvp => (kvp.Key, kvp.Value)) + .OrderBy(p => p.Key.GenderRace) + .ThenBy(p => p.Key.Type) + .ThenBy(p => p.Key.EntryIndex); + + protected override int Count + => Editor.Atch.Count; + + private bool DrawIdentifierInput(ref AtchIdentifier identifier) + { + var changes = false; + ImGui.TableNextColumn(); + changes |= DrawRace(ref identifier); + ImGui.TableNextColumn(); + changes |= DrawGender(ref identifier, false); + if (changes) + UpdateFile(); + ImGui.TableNextColumn(); + if (DrawPointInput(ref identifier, _combo)) + { + _currentBaseAtchPoint = _currentBaseAtchFile?.GetPoint(identifier.Type); + changes = true; + } + + ImGui.TableNextColumn(); + changes |= DrawEntryIndexInput(ref identifier, _currentBaseAtchPoint!); + + return changes; + } + + private void UpdateFile() + { + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; + _currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type); + if (_currentBaseAtchPoint == null) + { + _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); + Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; + } + + if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length) + Identifier = Identifier with { EntryIndex = 0 }; + } + + private static void DrawIdentifier(AtchIdentifier identifier) + { + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); + ImUtf8.HoverTooltip("Model Race"u8); + + ImGui.TableNextColumn(); + DrawGender(ref identifier, true); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); + ImUtf8.HoverTooltip("Attachment Point Type"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.EntryIndex.ToString(), FrameColor); + ImUtf8.HoverTooltip("State Entry Index"u8); + } + + private static bool DrawEntry(in AtchEntry defaultEntry, ref AtchEntry entry, bool disabled) + { + var changes = false; + using var dis = ImRaii.Disabled(disabled); + if (defaultEntry.Bone.Length == 0) + return false; + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##BoneName"u8, entry.FullSpan, out TerminatedByteString newBone)) + { + entry.SetBoneName(newBone); + changes = true; + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Bone Name"u8); + + ImGui.SetNextItemWidth(200 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchScale"u8, ref entry.Scale); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Scale"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetX"u8, ref entry.OffsetX); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Offset X-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationX"u8, ref entry.RotationX); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation X-Axis"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetY"u8, ref entry.OffsetY); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Offset Y-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationY"u8, ref entry.RotationY); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation Y-Axis"u8); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchOffsetZ"u8, ref entry.OffsetZ); + ImUtf8.HoverTooltip("Offset Z-Coordinate"u8); + ImGui.SetNextItemWidth(120 * ImUtf8.GlobalScale); + changes |= ImUtf8.InputScalar("##AtchRotationZ"u8, ref entry.RotationZ); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Rotation Z-Axis"u8); + + return changes; + } + + private static bool DrawRace(ref AtchIdentifier identifier, float unscaledWidth = 100) + { + var ret = Combos.Race("##atchRace", identifier.Race, out var race, unscaledWidth); + ImUtf8.HoverTooltip("Model Race"u8); + if (ret) + identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; + + return ret; + } + + private static bool DrawGender(ref AtchIdentifier identifier, bool disabled) + { + var isMale = identifier.Gender is Gender.Male; + + if (!ImUtf8.IconButton(isMale ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus, "Gender"u8, buttonColor: disabled ? 0x000F0000u : 0) + || disabled) + return false; + + identifier = identifier with { GenderRace = Names.CombinedRace(isMale ? Gender.Female : Gender.Male, identifier.Race) }; + return true; + } + + private static bool DrawPointInput(ref AtchIdentifier identifier, AtchPointCombo combo) + { + if (!combo.Draw("##AtchPoint", identifier.Type.ToName(), "Attachment Point Type", 160 * ImUtf8.GlobalScale, + ImGui.GetTextLineHeightWithSpacing())) + return false; + + identifier = identifier with { Type = combo.CurrentSelection }; + return true; + } + + private static bool DrawEntryIndexInput(ref AtchIdentifier identifier, AtchPoint currentAtchPoint) + { + var index = identifier.EntryIndex; + ImGui.SetNextItemWidth(40 * ImUtf8.GlobalScale); + var ret = ImUtf8.DragScalar("##AtchEntry"u8, ref index, 0, (ushort)(currentAtchPoint.Entries.Length - 1), 0.05f, + ImGuiSliderFlags.AlwaysClamp); + ImUtf8.HoverTooltip("State Entry Index"u8); + if (!ret) + return false; + + index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint!.Entries.Length - 1)); + identifier = identifier with { EntryIndex = index }; + return true; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs index f9baddbe..348a0d4c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqdpMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -34,7 +35,7 @@ public sealed class EqdpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFil protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqdp)); + CopyToClipboardButton("Copy all current EQDP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Eqdp))); ImGui.TableNextColumn(); var validRaceCode = CharacterUtilityData.EqdpIdx(Identifier.GenderRace, false) >= 0; diff --git a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs index 51b14459..d6df95cb 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EqpMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -34,7 +35,7 @@ public sealed class EqpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Eqp)); + CopyToClipboardButton("Copy all current EQP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Eqp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs index 09075319..e5e28a3d 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/EstMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -33,7 +34,7 @@ public sealed class EstMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Est)); + CopyToClipboardButton("Copy all current EST manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Est))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs index 1aa9060e..929feadd 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GlobalEqpMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.Meta; @@ -29,7 +30,7 @@ public sealed class GlobalEqpMetaDrawer(ModMetaEditor editor, MetaFileManager me protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.GlobalEqp)); + CopyToClipboardButton("Copy all current global EQP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.GlobalEqp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs index 9532d8e7..3691a4f7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/GmpMetaDrawer.cs @@ -8,6 +8,7 @@ using Penumbra.Meta.Files; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; +using Newtonsoft.Json.Linq; namespace Penumbra.UI.AdvancedWindow.Meta; @@ -32,7 +33,7 @@ public sealed class GmpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Gmp)); + CopyToClipboardButton("Copy all current Gmp manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Gmp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs index c8310cf7..34488a87 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ImcMetaDrawer.cs @@ -1,5 +1,6 @@ using Dalamud.Interface; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -35,7 +36,7 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Imc)); + CopyToClipboardButton("Copy all current IMC manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Imc))); ImGui.TableNextColumn(); var canAdd = _fileExists && !Editor.Contains(Identifier); var tt = canAdd ? "Stage this edit."u8 : !_fileExists ? "This IMC file does not exist."u8 : "This entry is already edited."u8; @@ -116,7 +117,6 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(identifier.EquipSlot.ToName(), FrameColor); ImUtf8.HoverTooltip("Equip Slot"u8); } - } private static bool DrawEntry(ImcEntry defaultEntry, ref ImcEntry entry, bool addDefault) @@ -161,8 +161,9 @@ public sealed class ImcMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { var (equipSlot, secondaryId) = type switch { - ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId) 0), - ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), + ObjectType.Equipment => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, (SecondaryId)0), + ObjectType.DemiHuman => (identifier.EquipSlot.IsEquipment() ? identifier.EquipSlot : EquipSlot.Head, + identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), ObjectType.Accessory => (identifier.EquipSlot.IsAccessory() ? identifier.EquipSlot : EquipSlot.Ears, (SecondaryId)0), _ => (EquipSlot.Unknown, identifier.SecondaryId == 0 ? 1 : identifier.SecondaryId), }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 75de20a7..4c9142d8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -14,8 +14,9 @@ namespace Penumbra.UI.AdvancedWindow.Meta; public interface IMetaDrawer { - public ReadOnlySpan Label { get; } - public int NumColumns { get; } + public ReadOnlySpan Label { get; } + public int NumColumns { get; } + public float ColumnHeight { get; } public void Draw(); } @@ -42,7 +43,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta using var id = ImUtf8.PushId((int)Identifier.Type); DrawNew(); - var height = ImUtf8.FrameHeightSpacing; + var height = ColumnHeight; var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); ImGuiClip.DrawEndDummy(remainder, height); @@ -54,6 +55,9 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta public abstract ReadOnlySpan Label { get; } public abstract int NumColumns { get; } + public virtual float ColumnHeight + => ImUtf8.FrameHeightSpacing; + protected abstract void DrawNew(); protected abstract void Initialize(); protected abstract void DrawEntry(TIdentifier identifier, TEntry entry); @@ -138,14 +142,14 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta protected void DrawMetaButtons(TIdentifier identifier, TEntry entry) { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy this manipulation to clipboard."u8, new JArray { MetaDictionary.Serialize(identifier, entry)! }); + CopyToClipboardButton("Copy this manipulation to clipboard."u8, new Lazy(() => new JArray { MetaDictionary.Serialize(identifier, entry)! })); ImGui.TableNextColumn(); if (ImUtf8.IconButton(FontAwesomeIcon.Trash, "Delete this meta manipulation."u8)) Editor.Changes |= Editor.Remove(identifier); } - protected void CopyToClipboardButton(ReadOnlySpan tooltip, JToken? manipulations) + protected void CopyToClipboardButton(ReadOnlySpan tooltip, Lazy manipulations) { if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index b3dd9299..d1c7cd52 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -10,7 +10,8 @@ public class MetaDrawers( GlobalEqpMetaDrawer globalEqp, GmpMetaDrawer gmp, ImcMetaDrawer imc, - RspMetaDrawer rsp) : IService + RspMetaDrawer rsp, + AtchMetaDrawer atch) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -19,6 +20,7 @@ public class MetaDrawers( public readonly RspMetaDrawer Rsp = rsp; public readonly ImcMetaDrawer Imc = imc; public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; + public readonly AtchMetaDrawer Atch = atch; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -29,7 +31,8 @@ public class MetaDrawers( MetaManipulationType.Est => Est, MetaManipulationType.Gmp => Gmp, MetaManipulationType.Rsp => Rsp, + MetaManipulationType.Atch => Atch, MetaManipulationType.GlobalEqp => GlobalEqp, - _ => null, + _ => null, }; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs index 87e8c5b8..d60f877b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/RspMetaDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using Newtonsoft.Json.Linq; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; @@ -33,7 +34,7 @@ public sealed class RspMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void DrawNew() { ImGui.TableNextColumn(); - CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, MetaDictionary.SerializeTo([], Editor.Rsp)); + CopyToClipboardButton("Copy all current RSP manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Rsp))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 49eac96e..22271d38 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -56,6 +56,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Est); DrawEditHeader(MetaManipulationType.Gmp); DrawEditHeader(MetaManipulationType.Rsp); + DrawEditHeader(MetaManipulationType.Atch); DrawEditHeader(MetaManipulationType.GlobalEqp); } From 10279fdc187d23c64ee158d89e3903137b085a00 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 26 Nov 2024 17:04:38 +0100 Subject: [PATCH 471/865] fix inverted hook logic. --- Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs | 5 ++--- Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index 748ca93a..07e34a66 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -1,4 +1,3 @@ -using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Game.Character; using OtterGui.Services; using Penumbra.GameData; @@ -19,7 +18,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo _metaState = metaState; _collectionResolver = collectionResolver; Task = hooks.CreateHook("AtchCaller1", Sigs.AtchCaller1, Detour, - metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller1); + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.AtchCaller1); if (!HookOverrides.Instance.Meta.AtchCaller1) _metaState.Config.ModsEnabled += Toggle; } @@ -30,7 +29,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo _metaState.AtchCollection.Push(collection); Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); - Penumbra.Log.Excessive( + Penumbra.Log.Information( $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); } diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs index 9b3349f2..aa2d3f31 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -18,7 +18,7 @@ public unsafe class AtchCallerHook2 : FastHook, IDispo _metaState = metaState; _collectionResolver = collectionResolver; Task = hooks.CreateHook("AtchCaller2", Sigs.AtchCaller2, Detour, - metaState.Config.EnableMods && HookOverrides.Instance.Meta.AtchCaller2); + metaState.Config.EnableMods && !HookOverrides.Instance.Meta.AtchCaller2); if (!HookOverrides.Instance.Meta.AtchCaller2) _metaState.Config.ModsEnabled += Toggle; } From 28250a9304f77be0dcfcc071f16a626e10785a0a Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 26 Nov 2024 16:11:29 +0000 Subject: [PATCH 472/865] [CI] Updating repo.json for testing_1.3.1.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index f5a8e2f1..bfea1e12 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.3", + "TestingAssemblyVersion": "1.3.1.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 8242cde15cfd5da17c3819157d3b61900bce92e2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:00:42 +0100 Subject: [PATCH 473/865] Don't spam logs. --- Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index 07e34a66..dcbaedc8 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -29,7 +29,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo _metaState.AtchCollection.Push(collection); Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); - Penumbra.Log.Information( + Penumbra.Log.Excessive( $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); } From 0aa8a44b8d878e6f383aeaf927d8237db32023ab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:00:57 +0100 Subject: [PATCH 474/865] Fix meta manipulation copy/paste. --- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 4c9142d8..2b9285ef 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -154,7 +154,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations.Value, MetaApi.CurrentVersion); if (text.Length > 0) ImGui.SetClipboardText(text); } From ac2631384f09c5bb927588f0133620e0dfd0a503 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:01:10 +0100 Subject: [PATCH 475/865] Fix mod reload of atch manipulations. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 7a5142dc..876fe12f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using OtterGui.Services; +using Penumbra.Collections.Cache; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -157,6 +158,23 @@ public class ModMetaEditor( } } + foreach (var (key, value) in clone.Atch) + { + var defaultEntry = AtchCache.GetDefault(metaFileManager, key); + if (!defaultEntry.HasValue) + continue; + + if (!defaultEntry.Value.Equals(value)) + { + dict.TryAdd(key, value); + } + else + { + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {key}."); + ++count; + } + } + if (count == 0) return false; From c8ad4bc1062fc9ec7885fcdcabed2e4d15c3efc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:01:42 +0100 Subject: [PATCH 476/865] Use meta transfer v1. --- Penumbra/Api/Api/MetaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 217cb1e3..871fe18b 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -53,7 +53,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } internal static string CompressMetaManipulations(ModCollection collection) - => CompressMetaManipulationsV0(collection); + => CompressMetaManipulationsV1(collection); private static string CompressMetaManipulationsV0(ModCollection collection) { From 242c0ee38fd0265db808e04d33e2c6d44768cf01 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:41:16 +0100 Subject: [PATCH 477/865] Add testing to IPC Meta. --- Penumbra/Api/Api/MetaApi.cs | 20 ++++++++++--------- Penumbra/Api/Api/TemporaryApi.cs | 4 ++-- Penumbra/Api/IpcTester/MetaIpcTester.cs | 16 ++++++++++++++- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 2 +- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 871fe18b..3f876bbf 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -146,11 +146,12 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver /// The empty string is treated as an empty set. /// Only returns true if all conversions are successful and distinct. /// - internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips) + internal static bool ConvertManips(string manipString, [NotNullWhen(true)] out MetaDictionary? manips, out byte version) { if (manipString.Length == 0) { - manips = new MetaDictionary(); + manips = new MetaDictionary(); + version = byte.MaxValue; return true; } @@ -163,9 +164,9 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver zipStream.CopyTo(resultStream); resultStream.Flush(); resultStream.Position = 0; - var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length); - var version = data[0]; - data = data[1..]; + var data = resultStream.GetBuffer().AsSpan(0, (int)resultStream.Length); + version = data[0]; + data = data[1..]; switch (version) { case 0: return ConvertManipsV0(data, out manips); @@ -179,7 +180,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver catch (Exception ex) { Penumbra.Log.Debug($"Error decompressing manipulations:\n{ex}"); - manips = null; + manips = null; + version = byte.MaxValue; return false; } } @@ -274,7 +276,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver var json = Encoding.UTF8.GetString(data); manips = JsonConvert.DeserializeObject(json); return manips != null; - } + } internal void TestMetaManipulations() { @@ -291,11 +293,11 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver var v1Time = watch.ElapsedMilliseconds; watch.Restart(); - var v1Success = ConvertManips(v1, out var v1Roundtrip); + var v1Success = ConvertManips(v1, out var v1Roundtrip, out _); var v1RoundtripTime = watch.ElapsedMilliseconds; watch.Restart(); - var v0Success = ConvertManips(v0, out var v0Roundtrip); + var v0Success = ConvertManips(v0, out var v0Roundtrip, out _); var v0RoundtripTime = watch.ElapsedMilliseconds; Penumbra.Log.Information($"Version | Count | Time | Length | Success | ReCount | ReTime | Equal"); diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 516b4347..201839e7 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -60,7 +60,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!MetaApi.ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m, out _)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, null, p, m, new ModPriority(priority)) switch @@ -86,7 +86,7 @@ public class TemporaryApi( if (!ConvertPaths(paths, out var p)) return ApiHelpers.Return(PenumbraApiEc.InvalidGamePath, args); - if (!MetaApi.ConvertManips(manipString, out var m)) + if (!MetaApi.ConvertManips(manipString, out var m, out _)) return ApiHelpers.Return(PenumbraApiEc.InvalidManipulation, args); var ret = tempMods.Register(tag, collection, p, m, new ModPriority(priority)) switch diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 8b393ade..010e3c5a 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -2,13 +2,19 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Api; using Penumbra.Api.IpcSubscribers; +using Penumbra.Meta.Manipulations; namespace Penumbra.Api.IpcTester; public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService { - private int _gameObjectIndex; + private int _gameObjectIndex; + private string _metaBase64 = string.Empty; + private MetaDictionary _metaDict = new(); + private byte _parsedVersion = byte.MaxValue; public void Draw() { @@ -17,6 +23,11 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService return; ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); + if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8)) + if (!MetaApi.ConvertManips(_metaBase64, out _metaDict, out _parsedVersion)) + _metaDict ??= new MetaDictionary(); + + using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -34,5 +45,8 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService var base64 = new GetMetaManipulations(pi).Invoke(_gameObjectIndex); ImGui.SetClipboardText(base64); } + + IpcTester.DrawIntro(string.Empty, "Parsed Data"); + ImUtf8.Text($"Version: {_parsedVersion}, Count: {_metaDict.Count}"); } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 2b9285ef..a6f042b7 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -154,7 +154,7 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta if (!ImUtf8.IconButton(FontAwesomeIcon.Clipboard, tooltip)) return; - var text = Functions.ToCompressedBase64(manipulations.Value, MetaApi.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations.Value, 0); if (text.Length > 0) ImGui.SetClipboardText(text); } From 9787e5a85228d28b686a301296a528747ca0a910 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 18:49:04 +0100 Subject: [PATCH 478/865] Fix some meta issues. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 22271d38..7d688df9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -111,7 +111,7 @@ public partial class ModEditWindow if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true)) return; - var text = Functions.ToCompressedBase64(manipulations, MetaApi.CurrentVersion); + var text = Functions.ToCompressedBase64(manipulations, 0); if (text.Length > 0) ImGui.SetClipboardText(text); } @@ -122,8 +122,7 @@ public partial class ModEditWindow { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaApi.CurrentVersion && manips != null) + if (MetaApi.ConvertManips(clipboard, out var manips, out _)) { _editor.MetaEditor.UpdateTo(manips); _editor.MetaEditor.Changes = true; @@ -139,8 +138,7 @@ public partial class ModEditWindow if (ImGui.Button("Set from Clipboard")) { var clipboard = ImGuiUtil.GetClipboardText(); - var version = Functions.FromCompressedBase64(clipboard, out var manips); - if (version == MetaApi.CurrentVersion && manips != null) + if (MetaApi.ConvertManips(clipboard, out var manips, out _)) { _editor.MetaEditor.SetTo(manips); _editor.MetaEditor.Changes = true; From 97d7ea7759898f721df7b743cb86d154813e71b5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 22:51:14 +0100 Subject: [PATCH 479/865] tmp --- Penumbra/Api/Api/MetaApi.cs | 27 ++++++++++--------- Penumbra/Api/IpcTester/MetaIpcTester.cs | 2 +- .../Processing/AtchPathPreProcessor.cs | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 3f876bbf..7c0cd5fc 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -16,8 +16,6 @@ namespace Penumbra.Api.Api; public class MetaApi(IFramework framework, CollectionResolver collectionResolver, ApiHelpers helpers) : IPenumbraApiMeta, IApiService { - public const int CurrentVersion = 1; - public string GetPlayerMetaManipulations() { var collection = collectionResolver.PlayerCollection(); @@ -99,7 +97,6 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver WriteCache(zipStream, cache.Est); WriteCache(zipStream, cache.Rsp); WriteCache(zipStream, cache.Gmp); - WriteCache(zipStream, cache.Atch); cache.GlobalEqp.EnterReadLock(); try @@ -112,6 +109,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver { cache.GlobalEqp.ExitReadLock(); } + + WriteCache(zipStream, cache.Atch); } } @@ -251,15 +250,6 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } - var atchCount = r.ReadInt32(); - for (var i = 0; i < atchCount; ++i) - { - var identifier = r.Read(); - var value = r.Read(); - if (!identifier.Validate() || !manips.TryAdd(identifier, value)) - return false; - } - var globalEqpCount = r.ReadInt32(); for (var i = 0; i < globalEqpCount; ++i) { @@ -268,6 +258,19 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver return false; } + // Atch was added after there were already some V1 around, so check for size here. + if (r.Position < r.Count) + { + var atchCount = r.ReadInt32(); + for (var i = 0; i < atchCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + } + return true; } diff --git a/Penumbra/Api/IpcTester/MetaIpcTester.cs b/Penumbra/Api/IpcTester/MetaIpcTester.cs index 010e3c5a..9cf20cd7 100644 --- a/Penumbra/Api/IpcTester/MetaIpcTester.cs +++ b/Penumbra/Api/IpcTester/MetaIpcTester.cs @@ -24,7 +24,7 @@ public class MetaIpcTester(IDalamudPluginInterface pi) : IUiService ImGui.InputInt("##metaIdx", ref _gameObjectIndex, 0, 0); if (ImUtf8.InputText("##metaText"u8, ref _metaBase64, "Base64 Metadata..."u8)) - if (!MetaApi.ConvertManips(_metaBase64, out _metaDict, out _parsedVersion)) + if (!MetaApi.ConvertManips(_metaBase64, out _metaDict!, out _parsedVersion)) _metaDict ??= new MetaDictionary(); diff --git a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs index 9a9096f3..428826bc 100644 --- a/Penumbra/Interop/Processing/AtchPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/AtchPathPreProcessor.cs @@ -20,7 +20,7 @@ public sealed class AtchPathPreProcessor : IPathPreProcessor if (!TryGetAtchGenderRace(path, out var gr)) return resolved; - Penumbra.Log.Information($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); + Penumbra.Log.Excessive($"Pre-Processed {path} with {resolveData.ModCollection} for {gr.ToName()}."); if (resolveData.ModCollection.MetaCache?.Atch.GetFile(gr, out var file) == true) return PathDataHandler.CreateAtch(path, resolveData.ModCollection); From 8b9f59426e3dba01e4f16267b05bf804ceca881d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Nov 2024 23:07:08 +0100 Subject: [PATCH 480/865] No V1 Meta yet... wait until next version ban or API increase. --- Penumbra/Api/Api/MetaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 7c0cd5fc..ff88ae4e 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -51,7 +51,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } internal static string CompressMetaManipulations(ModCollection collection) - => CompressMetaManipulationsV1(collection); + => CompressMetaManipulationsV0(collection); private static string CompressMetaManipulationsV0(ModCollection collection) { From d7095af89b322af9bd81acb80a9bab842fbee90f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 29 Nov 2024 17:33:05 +0100 Subject: [PATCH 481/865] Add jumping to mods in OnScreen tab. --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 3 + .../ResourceTree/ResourceTreeFactory.cs | 1 + .../UI/AdvancedWindow/ResourceTreeViewer.cs | 112 +++++++++--------- .../ResourceTreeViewerFactory.cs | 6 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 8 +- 5 files changed, 73 insertions(+), 57 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 6c3e1ebe..088527ca 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -1,4 +1,5 @@ using Penumbra.Api.Enums; +using Penumbra.Mods; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; @@ -16,6 +17,7 @@ public class ResourceNode : ICloneable public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; public string? ModName; + public readonly WeakReference Mod = new(null!); public string? ModRelativePath; public CiByteString AdditionalData; public readonly ulong Length; @@ -60,6 +62,7 @@ public class ResourceNode : ICloneable PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; ModName = other.ModName; + Mod = other.Mod; ModRelativePath = other.ModRelativePath; AdditionalData = other.AdditionalData; Length = other.Length; diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 9738148f..7e378f41 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -135,6 +135,7 @@ public class ResourceTreeFactory( if (node.FullPath.IsRooted && modManager.TryIdentifyPath(node.FullPath.FullName, out var mod, out var relativePath)) { node.ModName = mod.Name; + node.Mod.SetTarget(mod); node.ModRelativePath = relativePath; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3aff2ac9..7bad64f9 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -3,55 +3,40 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Text; +using Penumbra.Api.Enums; using Penumbra.Interop.ResourceTree; +using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.String; namespace Penumbra.UI.AdvancedWindow; -public class ResourceTreeViewer +public class ResourceTreeViewer( + Configuration config, + ResourceTreeFactory treeFactory, + ChangedItemDrawer changedItemDrawer, + IncognitoService incognito, + int actionCapacity, + Action onRefresh, + Action drawActions, + CommunicatorService communicator) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly Configuration _config; - private readonly ResourceTreeFactory _treeFactory; - private readonly ChangedItemDrawer _changedItemDrawer; - private readonly IncognitoService _incognito; - private readonly int _actionCapacity; - private readonly Action _onRefresh; - private readonly Action _drawActions; - private readonly HashSet _unfolded; + private readonly CommunicatorService _communicator = communicator; + private readonly HashSet _unfolded = []; - private readonly Dictionary _filterCache; + private readonly Dictionary _filterCache = []; - private TreeCategory _categoryFilter; - private ChangedItemIconFlag _typeFilter; - private string _nameFilter; - private string _nodeFilter; + private TreeCategory _categoryFilter = AllCategories; + private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; + private string _nameFilter = string.Empty; + private string _nodeFilter = string.Empty; private Task? _task; - public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - IncognitoService incognito, int actionCapacity, Action onRefresh, Action drawActions) - { - _config = config; - _treeFactory = treeFactory; - _changedItemDrawer = changedItemDrawer; - _incognito = incognito; - _actionCapacity = actionCapacity; - _onRefresh = onRefresh; - _drawActions = drawActions; - _unfolded = []; - - _filterCache = []; - - _categoryFilter = AllCategories; - _typeFilter = ChangedItemFlagExtensions.AllFlags; - _nameFilter = string.Empty; - _nodeFilter = string.Empty; - } - public void Draw() { DrawControls(); @@ -74,7 +59,7 @@ public class ResourceTreeViewer } else if (_task.IsCompletedSuccessfully) { - var debugMode = _config.DebugMode; + var debugMode = config.DebugMode; foreach (var (tree, index) in _task.Result.WithIndex()) { var category = Classify(tree); @@ -83,7 +68,7 @@ public class ResourceTreeViewer using (var c = ImRaii.PushColor(ImGuiCol.Text, CategoryColor(category).Value())) { - var isOpen = ImGui.CollapsingHeader($"{(_incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", + var isOpen = ImGui.CollapsingHeader($"{(incognito.IncognitoMode ? tree.AnonymizedName : tree.Name)}###{index}", index == 0 ? ImGuiTreeNodeFlags.DefaultOpen : 0); if (debugMode) { @@ -98,9 +83,9 @@ public class ResourceTreeViewer using var id = ImRaii.PushId(index); - ImGui.TextUnformatted($"Collection: {(_incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); + ImGui.TextUnformatted($"Collection: {(incognito.IncognitoMode ? tree.AnonymizedCollectionName : tree.CollectionName)}"); - using var table = ImRaii.Table("##ResourceTree", _actionCapacity > 0 ? 4 : 3, + using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); if (!table) continue; @@ -108,9 +93,9 @@ 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) + if (actionCapacity > 0) ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, - (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight()); + (actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); @@ -150,7 +135,7 @@ public class ResourceTreeViewer ImGui.SetCursorPosY(ImGui.GetCursorPosY() - yOffset); using (ImRaii.Child("##typeFilter", new Vector2(ImGui.GetContentRegionAvail().X, ChangedItemDrawer.TypeFilterIconSize.Y))) { - filterChanged |= _changedItemDrawer.DrawTypeFilter(ref _typeFilter); + filterChanged |= changedItemDrawer.DrawTypeFilter(ref _typeFilter); } var fieldWidth = (ImGui.GetContentRegionAvail().X - checkSpacing * 2.0f - ImGui.GetFrameHeightWithSpacing()) / 2.0f; @@ -160,7 +145,7 @@ public class ResourceTreeViewer ImGui.SetNextItemWidth(fieldWidth); filterChanged |= ImGui.InputTextWithHint("##NodeFilter", "Filter by Item/Part Name or Path...", ref _nodeFilter, 128); ImGui.SameLine(0, checkSpacing); - _incognito.DrawToggle(ImGui.GetFrameHeightWithSpacing()); + incognito.DrawToggle(ImGui.GetFrameHeightWithSpacing()); if (filterChanged) _filterCache.Clear(); @@ -171,7 +156,7 @@ public class ResourceTreeViewer { try { - return _treeFactory.FromObjectTable(ResourceTreeFactoryFlags) + return treeFactory.FromObjectTable(ResourceTreeFactoryFlags) .Select(entry => entry.ResourceTree) .ToArray(); } @@ -179,16 +164,16 @@ public class ResourceTreeViewer { _filterCache.Clear(); _unfolded.Clear(); - _onRefresh(); + onRefresh(); } }); private void DrawNodes(IEnumerable resourceNodes, int level, nint pathHash, ChangedItemIconFlag parentFilterIconFlag) { - var debugMode = _config.DebugMode; + var debugMode = config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f; + var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { @@ -231,7 +216,7 @@ public class ResourceTreeViewer ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); } - _changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); + changedItemDrawer.DrawCategoryIcon(resourceNode.IconFlag); ImGui.SameLine(0f, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.TableHeader(resourceNode.Name); if (ImGui.IsItemClicked() && unfoldable) @@ -270,14 +255,33 @@ public class ResourceTreeViewer ImGui.TableNextColumn(); if (resourceNode.FullPath.FullName.Length > 0) { - var uiFullPathStr = resourceNode.ModName != null && resourceNode.ModRelativePath != null - ? $"[{resourceNode.ModName}] {resourceNode.ModRelativePath}" - : resourceNode.FullPath.ToPath(); - ImGui.Selectable(uiFullPathStr, false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + var hasMod = resourceNode.Mod.TryGetTarget(out var mod); + if (resourceNode is { ModName: not null, ModRelativePath: not null }) + { + var modName = $"[{(hasMod ? mod!.Name : resourceNode.ModName)}]"; + var textPos = ImGui.GetCursorPosX() + ImUtf8.CalcTextSize(modName).X + ImGui.GetStyle().ItemInnerSpacing.X; + 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)); + } + + ImGui.SameLine(); + ImGui.SetCursorPosX(textPos); + ImUtf8.Text(resourceNode.ModRelativePath); + } + else + { + ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + } + if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); + if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) + _communicator.SelectTab.Invoke(TabType.Mods, mod); + ImGuiUtil.HoverTooltip( - $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } else { @@ -289,12 +293,12 @@ public class ResourceTreeViewer mutedColor.Dispose(); - if (_actionCapacity > 0) + 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)); + drawActions(resourceNode, new Vector2(frameHeight)); } if (unfolded) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index ea64c0bf..10a4aea2 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,5 +1,6 @@ using OtterGui.Services; using Penumbra.Interop.ResourceTree; +using Penumbra.Services; namespace Penumbra.UI.AdvancedWindow; @@ -7,8 +8,9 @@ public class ResourceTreeViewerFactory( Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer, - IncognitoService incognito) : IService + IncognitoService incognito, + CommunicatorService communicator) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions); + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index fc735d04..46e427ed 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -461,7 +461,7 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; - using var table = Table("##actors", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; @@ -485,6 +485,9 @@ public class DebugTab : Window, ITab, IUiService ? $"{identifier.DataId} | {obj.AsObject->BaseId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{(*(nint*)obj.Address):X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" : "NULL"); } return; @@ -499,6 +502,9 @@ public class DebugTab : Window, ITab, IUiService ImGuiUtil.DrawTableColumn(string.Empty); ImGuiUtil.DrawTableColumn(_actors.ToString(id)); ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(string.Empty); } } From b377ca372ce2d349292b89c467948f659e312c24 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 29 Nov 2024 16:35:08 +0000 Subject: [PATCH 482/865] [CI] Updating repo.json for testing_1.3.1.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index bfea1e12..59ca83be 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.4", + "TestingAssemblyVersion": "1.3.1.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 22541b3fd894a244ee8160845ce52d9939c29c1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Dec 2024 20:23:15 +0100 Subject: [PATCH 483/865] Update variables drawer. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 160 +++--------------- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 126 ++++++++++++++ 2 files changed, 154 insertions(+), 132 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 46e427ed..30605101 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -6,7 +6,6 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using ImGuiNET; @@ -35,8 +34,6 @@ using Penumbra.UI.Classes; using Penumbra.Util; using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; -using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.Interop.Hooks.PostProcessing; @@ -80,8 +77,7 @@ public class DebugTab : Window, ITab, IUiService private readonly HttpApi _httpApi; private readonly ActorManager _actors; private readonly StainService _stains; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; + private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly ResourceManagerService _resourceManager; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; @@ -104,18 +100,17 @@ public class DebugTab : Window, ITab, IUiService private readonly CrashHandlerPanel _crashHandlerPanel; private readonly TexHeaderDrawer _texHeaderDrawer; private readonly HookOverrideDrawer _hookOverrides; - private readonly RsfService _rsfService; + private readonly RsfService _rsfService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, - CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, RsfService rsfService) + HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -132,8 +127,6 @@ public class DebugTab : Window, ITab, IUiService _httpApi = httpApi; _actors = actors; _stains = stains; - _characterUtility = characterUtility; - _residentResources = residentResources; _resourceManager = resourceManager; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; @@ -153,7 +146,8 @@ public class DebugTab : Window, ITab, IUiService _crashHandlerPanel = crashHandlerPanel; _texHeaderDrawer = texHeaderDrawer; _hookOverrides = hookOverrides; - _rsfService = rsfService; + _rsfService = rsfService; + _globalVariablesDrawer = globalVariablesDrawer; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -185,14 +179,13 @@ public class DebugTab : Window, ITab, IUiService DrawActorsDebug(); DrawCollectionCaches(); _texHeaderDrawer.Draw(); - DrawDebugCharacterUtility(); DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); DrawResourceProblems(); _hookOverrides.Draw(); DrawPlayerModelInfo(); - DrawGlobalVariableInfo(); + _globalVariablesDrawer.Draw(); DrawDebugTabIpc(); } @@ -217,8 +210,10 @@ public class DebugTab : Window, ITab, IUiService { if (resourceNode) foreach (var (path, resource) in collection._cache!.CustomResources) + { ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); + } } using var modNode = ImUtf8.TreeNode("Enabled Mods"u8); @@ -485,9 +480,11 @@ public class DebugTab : Window, ITab, IUiService ? $"{identifier.DataId} | {obj.AsObject->BaseId}" : identifier.DataId.ToString(); ImGuiUtil.DrawTableColumn(id); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{(*(nint*)obj.Address):X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{*(nint*)obj.Address:X}" : "NULL"); ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero + ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" + : "NULL"); } return; @@ -795,68 +792,6 @@ public class DebugTab : Window, ITab, IUiService } } - /// - /// Draw information about the character utility class from SE, - /// displaying all files, their sizes, the default files and the default sizes. - /// - private unsafe void DrawDebugCharacterUtility() - { - if (!ImGui.CollapsingHeader("Character Utility")) - return; - - using var table = Table("##CharacterUtility", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) - { - var intern = CharacterUtility.ReverseIndices[idx]; - var resource = _characterUtility.Address->Resource(idx); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"[{idx}]"); - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{(ulong)resource:X}"); - ImGui.TableNextColumn(); - if (resource == null) - { - ImGui.TableNextRow(); - continue; - } - - UiHelpers.Text(resource); - ImGui.TableNextColumn(); - var data = (nint)resource->CsHandle.GetData(); - var length = resource->CsHandle.GetLength(); - if (ImGui.Selectable($"0x{data:X}")) - if (data != nint.Zero && length > 0) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)data, (int)length).ToArray().Select(b => b.ToString("X2")))); - - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(length.ToString()); - - ImGui.TableNextColumn(); - if (intern.Value != -1) - { - ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText(string.Join("\n", - new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, - _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); - - ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); - } - else - { - ImGui.TableNextColumn(); - } - } - } private void DrawShaderReplacementFixer() { @@ -946,45 +881,6 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted($"{slowPathCallDeltas.Skin}"); } - /// Draw information about the resident resource files. - private unsafe void DrawDebugResidentResources() - { - using var tree = TreeNode("Resident Resources"); - if (!tree) - return; - - if (_residentResources.Address == null || _residentResources.Address->NumResources == 0) - return; - - using var table = Table("##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - for (var i = 0; i < _residentResources.Address->NumResources; ++i) - { - var resource = _residentResources.Address->ResourceList[i]; - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"0x{(ulong)resource:X}"); - ImGui.TableNextColumn(); - UiHelpers.Text(resource); - } - } - - private static void DrawCopyableAddress(string label, nint address) - { - using (var _ = PushFont(UiBuilder.MonoFont)) - { - if (ImGui.Selectable($"0x{address:X16} {label}")) - ImGui.SetClipboardText($"{address:X16}"); - } - - ImGuiUtil.HoverTooltip("Click to copy address to clipboard."); - } - - private static unsafe void DrawCopyableAddress(string label, void* address) - => DrawCopyableAddress(label, (nint)address); - /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { @@ -993,13 +889,13 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; - DrawCopyableAddress("PlayerCharacter", player.Address); + DrawCopyableAddress("PlayerCharacter"u8, player.Address); var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); if (model == null) return; - DrawCopyableAddress("CharacterBase", model); + DrawCopyableAddress("CharacterBase"u8, model); using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) { @@ -1054,20 +950,6 @@ public class DebugTab : Window, ITab, IUiService } } - /// Draw information about some game global variables. - private unsafe void DrawGlobalVariableInfo() - { - var header = ImGui.CollapsingHeader("Global Variables"); - ImGuiUtil.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."); - if (!header) - return; - - DrawCopyableAddress("CharacterUtility", _characterUtility.Address); - DrawCopyableAddress("ResidentResourceManager", _residentResources.Address); - DrawCopyableAddress("Device", Device.Instance()); - DrawDebugResidentResources(); - } - private string _crcInput = string.Empty; private FullPath _crcPath = FullPath.Empty; @@ -1169,4 +1051,18 @@ public class DebugTab : Window, ITab, IUiService _config.Ephemeral.DebugSeparateWindow = false; _config.Ephemeral.Save(); } + + 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); + } + + public static unsafe void DrawCopyableAddress(ReadOnlySpan label, nint address) + => DrawCopyableAddress(label, (void*)address); } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs new file mode 100644 index 00000000..601e9b4d --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -0,0 +1,126 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Services; + +namespace Penumbra.UI.Tabs.Debug; + +public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, ResidentResourceManager residentResources) : IUiService +{ + /// Draw information about some game global variables. + public void Draw() + { + var header = ImUtf8.CollapsingHeader("Global Variables"u8); + ImUtf8.HoverTooltip("Draw information about global variables. Can provide useful starting points for a memory viewer."u8); + if (!header) + return; + + DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); + DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); + DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); + DrawCharacterUtility(); + DrawResidentResources(); + } + + /// + /// Draw information about the character utility class from SE, + /// displaying all files, their sizes, the default files and the default sizes. + /// + private void DrawCharacterUtility() + { + using var tree = ImUtf8.TreeNode("Character Utility"u8); + if (!tree) + return; + + using var table = ImUtf8.Table("##CharacterUtility"u8, 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var idx = 0; idx < CharacterUtility.ReverseIndices.Length; ++idx) + { + var intern = CharacterUtility.ReverseIndices[idx]; + var resource = characterUtility.Address->Resource(idx); + ImUtf8.DrawTableColumn($"[{idx}]"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + if (resource == null) + { + ImGui.TableNextRow(); + continue; + } + + ImUtf8.DrawTableColumn(resource->CsHandle.FileName.AsSpan()); + 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); + 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); + + ImUtf8.DrawTableColumn($"{characterUtility.DefaultResource(intern).Size}"); + } + else + { + ImGui.TableNextColumn(); + } + } + } + + /// Draw information about the resident resource files. + private void DrawResidentResources() + { + using var tree = ImUtf8.TreeNode("Resident Resources"u8); + if (!tree) + return; + + if (residentResources.Address == null || residentResources.Address->NumResources == 0) + return; + + using var table = ImUtf8.Table("##ResidentResources"u8, 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var idx = 0; idx < residentResources.Address->NumResources; ++idx) + { + var resource = residentResources.Address->ResourceList[idx]; + ImUtf8.DrawTableColumn($"[{idx}]"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + if (resource == null) + { + ImGui.TableNextRow(); + continue; + } + + ImUtf8.DrawTableColumn(resource->CsHandle.FileName.AsSpan()); + 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); + ImUtf8.DrawTableColumn(length.ToString()); + } + } +} From 1434ad6190f0afb145eb502cad065131b50d600e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 5 Dec 2024 20:35:53 +0100 Subject: [PATCH 484/865] Add context menu copying for paths in advanced editing. --- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 63 +++++++++++++++---- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index b07633b6..5cabd14b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,4 +1,3 @@ -using System.Linq; using Dalamud.Interface; using ImGuiNET; using OtterGui; @@ -15,6 +14,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { private readonly HashSet _selectedFiles = new(256); + private readonly HashSet _cutPaths = []; private LowerString _fileFilter = LowerString.Empty; private bool _showGamePaths = true; private string _gamePathEdit = string.Empty; @@ -125,7 +125,7 @@ public partial class ModEditWindow using var id = ImRaii.PushId(i); ImGui.TableNextColumn(); - DrawSelectable(registry); + DrawSelectable(registry, i); if (!_showGamePaths) continue; @@ -177,24 +177,63 @@ public partial class ModEditWindow } } - private void DrawSelectable(FileRegistry registry) + private void DrawSelectable(FileRegistry registry, int i) { var selected = _selectedFiles.Contains(registry); var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; - using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value()); - if (UiHelpers.Selectable(registry.RelPath.Path, selected)) + using (ImRaii.PushColor(ImGuiCol.Text, color.Value())) { - if (selected) - _selectedFiles.Remove(registry); - else - _selectedFiles.Add(registry); + if (UiHelpers.Selectable(registry.RelPath.Path, selected)) + { + if (selected) + _selectedFiles.Remove(registry); + else + _selectedFiles.Add(registry); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.OpenPopup("context"u8); + + var rightText = DrawFileTooltip(registry, color); + + ImGui.SameLine(); + ImGuiUtil.RightAlign(rightText); } - var rightText = DrawFileTooltip(registry, color); + DrawContextMenu(registry, i); + } - ImGui.SameLine(); - ImGuiUtil.RightAlign(rightText); + private void DrawContextMenu(FileRegistry registry, int i) + { + using var context = ImUtf8.Popup("context"u8); + if (!context) + return; + + using (ImRaii.Disabled(registry.CurrentUsage == 0)) + { + if (ImUtf8.Selectable("Cut Game Paths"u8)) + { + _cutPaths.Clear(); + for (var j = 0; j < registry.SubModUsage.Count; ++j) + { + if (registry.SubModUsage[j].Item1 != _editor.Option) + continue; + + _cutPaths.Add(registry.SubModUsage[j].Item2); + _editor.FileEditor.SetGamePath(_editor.Option, i, j--, Utf8GamePath.Empty); + } + } + } + + using (ImRaii.Disabled(_cutPaths.Count == 0)) + { + if (ImUtf8.Selectable("Paste Game Paths"u8)) + { + foreach (var path in _cutPaths) + _editor.FileEditor.SetGamePath(_editor.Option!, i, -1, path); + } + } } private void PrintGamePath(int i, int j, FileRegistry registry, IModDataContainer subMod, Utf8GamePath gamePath) From e9014fe4c3de3d3f24adb96cd9d4d84e6d045960 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 5 Dec 2024 19:38:51 +0000 Subject: [PATCH 485/865] [CI] Updating repo.json for testing_1.3.1.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 59ca83be..50838b67 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.1.3", - "TestingAssemblyVersion": "1.3.1.5", + "TestingAssemblyVersion": "1.3.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 01db37cbd4724e173433e01d7ec381a7ae356569 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Dec 2024 21:22:21 +0100 Subject: [PATCH 486/865] Add Copy for paths, update npc names --- Penumbra.GameData | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2b0c7f3b..da74a4be 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2b0c7f3bee0bc2eb466540d2fac265804354493d +Subproject commit da74a4be9c9728c6c52134c42603cd8a7040c568 diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 5cabd14b..6792c359 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -210,6 +210,21 @@ public partial class ModEditWindow if (!context) return; + using (ImRaii.Disabled(registry.CurrentUsage == 0)) + { + if (ImUtf8.Selectable("Copy Game Paths"u8)) + { + _cutPaths.Clear(); + for (var j = 0; j < registry.SubModUsage.Count; ++j) + { + if (registry.SubModUsage[j].Item1 != _editor.Option) + continue; + + _cutPaths.Add(registry.SubModUsage[j].Item2); + } + } + } + using (ImRaii.Disabled(registry.CurrentUsage == 0)) { if (ImUtf8.Selectable("Cut Game Paths"u8)) From 4cc7d1930b04d951e0255e2c9b82395ed51ea2b9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 9 Dec 2024 21:30:00 +0100 Subject: [PATCH 487/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index da74a4be..fb692d13 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit da74a4be9c9728c6c52134c42603cd8a7040c568 +Subproject commit fb692d13205fed5e6c5f4c939477c28473198a3b From 22c3b3b629c3d997f0a6a7d689196977d0e5b6b2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 15:43:09 +0100 Subject: [PATCH 488/865] Again. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index fb692d13..315258f4 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit fb692d13205fed5e6c5f4c939477c28473198a3b +Subproject commit 315258f4f8a59d744aa4d2d1f8c31d410d041729 From 08ff9b679e86d3ab9d76f64c781cf83989617cb5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 17:48:54 +0100 Subject: [PATCH 489/865] Add changing mod settings to command / macro API. --- Penumbra/Api/Api/ModSettingsApi.cs | 70 +++++++++++++++++------------- Penumbra/CommandHandler.cs | 61 ++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index e046ce30..8c34c249 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -184,36 +184,9 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); - if (groupIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - - var setting = Setting.Zero; - switch (mod.Groups[groupIdx]) - { - case { Behaviour: GroupDrawBehaviour.SingleSelection } single: - { - var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting = Setting.Single(optionIdx); - break; - } - case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: - { - foreach (var name in optionNames) - { - var optionIdx = multi.Options.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting |= Setting.Multi(optionIdx); - } - - break; - } - } + var settingSuccess = ConvertModSetting(mod, optionGroupName, optionNames, out var groupIdx, out var setting); + if (settingSuccess is not PenumbraApiEc.Success) + return ApiHelpers.Return(settingSuccess, args); var ret = _collectionEditor.SetModSetting(collection, mod, groupIdx, setting) ? PenumbraApiEc.Success @@ -283,4 +256,41 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable TriggerSettingEdited(mod); } + + public static PenumbraApiEc ConvertModSetting(Mod mod, string groupName, IReadOnlyList optionNames, out int groupIndex, + out Setting setting) + { + groupIndex = mod.Groups.IndexOf(g => g.Name.Equals(groupName, StringComparison.OrdinalIgnoreCase)); + setting = Setting.Zero; + if (groupIndex < 0) + return PenumbraApiEc.OptionGroupMissing; + + switch (mod.Groups[groupIndex]) + { + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return PenumbraApiEc.OptionMissing; + + setting = Setting.Single(optionIdx); + break; + } + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.Options.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return PenumbraApiEc.OptionMissing; + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + return PenumbraApiEc.Success; + } } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index db8d9aca..9c3eb988 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -4,6 +4,7 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.Api.Api; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -379,16 +380,18 @@ public class CommandHandler : IDisposable, IApiService if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]") + .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|setting]").AddText(" ") + .AddYellow("[Collection Name]") .AddText(" | ") - .AddPurple("[Mod Name or Mod Directory Name]"); + .AddPurple("[Mod Name or Mod Directory Name]") + .AddGreen(" <| [Option Group Name] | [Option1;Option2;...]>"); _chat.Print(seString.BuiltString); return true; } var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var nameSplit = split.Length != 2 - ? Array.Empty() + ? [] : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (nameSplit.Length != 2) { @@ -406,6 +409,23 @@ public class CommandHandler : IDisposable, IApiService if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; + var groupName = string.Empty; + var optionNames = Array.Empty(); + if (state is 4) + { + var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split2.Length < 2) + { + _chat.Print("Not enough arguments for changing settings provided."); + return false; + } + + nameSplit[1] = split2[0]; + groupName = split2[1]; + if (split2.Length == 3) + optionNames = split2[2].Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + } + if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod)) { _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.") @@ -413,12 +433,35 @@ public class CommandHandler : IDisposable, IApiService return false; } - if (HandleModState(state, collection!, mod)) - return true; + if (state < 4) + { + if (HandleModState(state, collection!, mod)) + return true; + + _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) + .AddText("already had the desired state in collection ") + .AddYellow(collection!.Name, true).AddText(".").BuiltString); + return false; + } + + switch (ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIndex, out var setting)) + { + case PenumbraApiEc.OptionGroupMissing: + _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" has no group ") + .AddGreen(groupName, true).AddText(".").BuiltString); + return false; + case PenumbraApiEc.OptionMissing: + _chat.Print(new SeStringBuilder().AddText("Not all set options in the mod ").AddRed(nameSplit[1], true) + .AddText(" could be found in group ").AddGreen(groupName, true).AddText(".").BuiltString); + return false; + case PenumbraApiEc.Success: + _collectionEditor.SetModSetting(collection!, mod, groupIndex, setting); + Print(() => new SeStringBuilder().AddText("Changed settings of group ").AddGreen(groupName, true).AddText(" in mod ") + .AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection!.Name, true).AddText(".").BuiltString); + return true; + } - _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) - .AddText("already had the desired state in collection ") - .AddYellow(collection!.Name, true).AddText(".").BuiltString); return false; } @@ -556,6 +599,8 @@ public class CommandHandler : IDisposable, IApiService "toggle" => 2, "inherit" => 3, "inherited" => 3, + "setting" => 4, + "settings" => 4, _ => -1, }; From 5db3d53994d5a7beb0b617078f3becfe04ad5010 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 17:56:27 +0100 Subject: [PATCH 490/865] Small improvements. --- Penumbra/CommandHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 9c3eb988..aff7f16f 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -380,7 +380,7 @@ public class CommandHandler : IDisposable, IApiService if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|setting]").AddText(" ") + .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle|").AddGreen("setting").AddBlue("]").AddText(" ") .AddYellow("[Collection Name]") .AddText(" | ") .AddPurple("[Mod Name or Mod Directory Name]") @@ -416,7 +416,7 @@ public class CommandHandler : IDisposable, IApiService var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (split2.Length < 2) { - _chat.Print("Not enough arguments for changing settings provided."); + _chat.Print("Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options."); return false; } From 510b9a5f1f638151eab715f0f9eb7c09b990cff0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Dec 2024 18:11:17 +0100 Subject: [PATCH 491/865] 1.3.2.0 --- Penumbra/UI/Changelog.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 0b0ca81a..ec2a716c 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -55,10 +55,24 @@ public class PenumbraChangelog : IUiService Add1_2_1_0(Changelog); Add1_3_0_0(Changelog); Add1_3_1_0(Changelog); - } - + Add1_3_2_0(Changelog); + } + #region Changelogs + private static void Add1_3_2_0(Changelog log) + => log.NextVersion("Version 1.3.2.0") + .RegisterHighlight("Added ATCH meta manipulations that allow the composite editing of attachment points across multiple mods.") + .RegisterEntry("Those ATCH manipulations should be shared via Mare Synchronos.", 1) + .RegisterEntry("This is an early implementation and might be bug-prone. Let me know of any issues. It was in testing for quite a while without reports.", 1) + .RegisterEntry("Added jumping to identified mods in the On-Screen tab via Control + Right-Click and improved their display slightly.") + .RegisterEntry("Added some right-click context menu copy options in the File Redirections editor for paths.") + .RegisterHighlight("Added the option to change a specific mod's settings via chat commands by using '/penumbra mod settings'.") + .RegisterEntry("Fixed issues with the copy-pasting of meta manipulations.") + .RegisterEntry("Fixed some other issues related to meta manipulations.") + .RegisterEntry("Updated available NPC names and fixed an issue with some supposedly invisible characters in names showing in ImGui."); + + private static void Add1_3_1_0(Changelog log) => log.NextVersion("Version 1.3.1.0") .RegisterEntry("Penumbra has been updated for Dalamud API 11 and patch 7.1.") From b5a469c5245d43c2831fa692d5f54b7f79c4fb24 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 13 Dec 2024 17:13:38 +0000 Subject: [PATCH 492/865] [CI] Updating repo.json for 1.3.2.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 50838b67..c0e561da 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.3.1.3", - "TestingAssemblyVersion": "1.3.1.6", + "AssemblyVersion": "1.3.2.0", + "TestingAssemblyVersion": "1.3.2.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.1.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.1.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From cc97ea0ce938a6c4f561c01991249cd768a613ae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 16 Dec 2024 17:52:57 +0100 Subject: [PATCH 493/865] Add an option to automatically select the collection assigned to the current character on login. --- .../Collections/CollectionAutoSelector.cs | 75 +++++++++++++++++++ Penumbra/Configuration.cs | 4 +- Penumbra/Services/MessageService.cs | 3 +- Penumbra/UI/Changelog.cs | 1 - Penumbra/UI/Tabs/SettingsTab.cs | 15 +++- 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 Penumbra/Collections/CollectionAutoSelector.cs diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs new file mode 100644 index 00000000..e24fa6a9 --- /dev/null +++ b/Penumbra/Collections/CollectionAutoSelector.cs @@ -0,0 +1,75 @@ +using Dalamud.Plugin.Services; +using OtterGui.Services; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Collections; + +public sealed class CollectionAutoSelector : IService, IDisposable +{ + private readonly Configuration _config; + private readonly ActiveCollections _collections; + private readonly IClientState _clientState; + private readonly CollectionResolver _resolver; + private readonly ObjectManager _objects; + + public CollectionAutoSelector(Configuration config, ActiveCollections collections, IClientState clientState, CollectionResolver resolver, + ObjectManager objects) + { + _config = config; + _collections = collections; + _clientState = clientState; + _resolver = resolver; + _objects = objects; + + if (_config.AutoSelectCollection) + Attach(); + } + + public bool Disposed { get; private set; } + + public void SetAutomaticSelection(bool value) + { + _config.AutoSelectCollection = value; + if (value) + Attach(); + else + Detach(); + } + + private void Attach() + { + if (Disposed) + return; + + _clientState.Login += OnLogin; + Select(); + } + + private void OnLogin() + => Select(); + + private void Detach() + => _clientState.Login -= OnLogin; + + private void Select() + { + if (!_objects[0].IsCharacter) + return; + + var collection = _resolver.PlayerCollection(); + Penumbra.Log.Debug($"Setting current collection to {collection.Identifier} through automatic collection selection."); + _collections.SetCollection(collection, CollectionType.Current); + } + + + public void Dispose() + { + if (Disposed) + return; + + Disposed = true; + Detach(); + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 50426b38..ec5784f8 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -56,6 +56,8 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideUiWhenUiHidden { get; set; } = false; public bool UseDalamudUiTextureRedirection { get; set; } = true; + public bool AutoSelectCollection { get; set; } = false; + public bool ShowModsInLobby { get; set; } = true; public bool UseCharacterCollectionInMainWindow { get; set; } = true; public bool UseCharacterCollectionsInCards { get; set; } = true; @@ -100,7 +102,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool UseFileSystemCompression { get; set; } = true; public bool EnableHttpApi { get; set; } = true; - public bool MigrateImportedModelsToV6 { get; set; } = true; + public bool MigrateImportedModelsToV6 { get; set; } = true; public bool MigrateImportedMaterialsToLegacy { get; set; } = true; public string DefaultModImportPath { get; set; } = string.Empty; diff --git a/Penumbra/Services/MessageService.cs b/Penumbra/Services/MessageService.cs index e610cb6a..70ccf47b 100644 --- a/Penumbra/Services/MessageService.cs +++ b/Penumbra/Services/MessageService.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using OtterGui.Log; using OtterGui.Services; +using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.String.Classes; using Notification = OtterGui.Classes.Notification; @@ -29,7 +30,7 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti new TextPayload($"{(char)SeIconChar.LinkMarker}"), new UIForegroundPayload(0), new UIGlowPayload(0), - new TextPayload(item.Name.ExtractText()), + new TextPayload(item.Name.ExtractTextExtended()), new RawPayload([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]), new RawPayload([0x02, 0x13, 0x02, 0xEC, 0x03]), }; diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index ec2a716c..c78ca290 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -92,7 +92,6 @@ public class PenumbraChangelog : IUiService private static void Add1_3_0_0(Changelog log) => log.NextVersion("Version 1.3.0.0") - .RegisterHighlight("The textures tab in the advanced editing window can now import and export .tga files.") .RegisterEntry("BC4 and BC6 textures can now also be imported.", 1) .RegisterHighlight("Added item swapping from and to the Glasses slot.") diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 9d8ea21c..46e214cf 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.Widgets; using Penumbra.Api; +using Penumbra.Collections; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -46,6 +47,7 @@ public class SettingsTab : ITab, IUiService private readonly PredefinedTagManager _predefinedTagManager; private readonly CrashHandlerService _crashService; private readonly MigrationSectionDrawer _migrationDrawer; + private readonly CollectionAutoSelector _autoSelector; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -57,7 +59,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector) { _pluginInterface = pluginInterface; _config = config; @@ -80,6 +82,7 @@ public class SettingsTab : ITab, IUiService _predefinedTagManager = predefinedTagConfig; _crashService = crashService; _migrationDrawer = migrationDrawer; + _autoSelector = autoSelector; } public void DrawHeader() @@ -421,6 +424,10 @@ public class SettingsTab : ITab, IUiService /// Draw all settings that do not fit into other categories. private void DrawMiscSettings() { + Checkbox("Automatically Select Character-Associated Collection", + "On every login, automatically select the collection associated with the current character as the current collection for editing.", + _config.AutoSelectCollection, _autoSelector.SetAutomaticSelection); + Checkbox("Print Chat Command Success Messages to Chat", "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", _config.PrintSuccessfulCommandsToChat, v => _config.PrintSuccessfulCommandsToChat = v); @@ -816,13 +823,15 @@ public class SettingsTab : ITab, IUiService if (ImGuiUtil.DrawDisabledButton("Compress Existing Files", Vector2.Zero, "Try to compress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, true); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.Xpress8K, + true); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Decompress Existing Files", Vector2.Zero, "Try to decompress all files in your root directory. This will take a while.", _compactor.MassCompactRunning || !_modManager.Valid)) - _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, true); + _compactor.StartMassCompact(_modManager.BasePath.EnumerateFiles("*.*", SearchOption.AllDirectories), CompressionAlgorithm.None, + true); if (_compactor.MassCompactRunning) { From 18288815b294ce54549da904b40a6bb0c09dd854 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 17 Dec 2024 18:04:17 +0100 Subject: [PATCH 494/865] Add partial copying of color and colordye tables. --- Penumbra.GameData | 2 +- .../Materials/MtrlTab.CommonColorTable.cs | 62 +++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 315258f4..6848397d 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 315258f4f8a59d744aa4d2d1f8c31d410d041729 +Subproject commit 6848397dd77cfcdbff1accd860d5b7e95f8c9fe5 diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 38f02100..236a66c3 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -202,24 +202,74 @@ public partial class MtrlTab if (Mtrl.Table == null) return false; - if (!ImUtf8.IconButton(FontAwesomeIcon.Paste, "Import an exported row from your clipboard onto this row."u8, + if (ImUtf8.IconButton(FontAwesomeIcon.Paste, + "Import an exported row from your clipboard onto this row.\n\nRight-Click for more options."u8, ImGui.GetFrameHeight() * Vector2.One, disabled)) + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String(text); + var row = Mtrl.Table.RowAsBytes(rowIdx); + var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; + if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + return false; + + data.AsSpan(0, row.Length).TryCopyTo(row); + data.AsSpan(row.Length).TryCopyTo(dyeRow); + + UpdateColorTableRowPreview(rowIdx); + + return true; + } + catch + { + return false; + } + + return ColorTablePasteFromClipboardContext(rowIdx, disabled); + } + + private unsafe bool ColorTablePasteFromClipboardContext(int rowIdx, bool disabled) + { + if (!disabled && ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.OpenPopup("context"u8); + + using var context = ImUtf8.Popup("context"u8); + if (!context) + return false; + + using var _ = ImRaii.Disabled(disabled); + + IColorTable.ValueTypes copy = 0; + IColorDyeTable.ValueTypes dyeCopy = 0; + if (ImUtf8.Selectable("Import Colors Only"u8)) + { + copy = IColorTable.ValueTypes.Colors; + dyeCopy = IColorDyeTable.ValueTypes.Colors; + } + + if (ImUtf8.Selectable("Import Other Values Only"u8)) + { + copy = ~IColorTable.ValueTypes.Colors; + dyeCopy = ~IColorDyeTable.ValueTypes.Colors; + } + + if (copy == 0) return false; try { var text = ImGui.GetClipboardText(); var data = Convert.FromBase64String(text); - var row = Mtrl.Table.RowAsBytes(rowIdx); + var row = Mtrl.Table!.RowAsHalves(rowIdx); + var halves = new Span(Unsafe.AsPointer(ref data[0]), row.Length); var dyeRow = Mtrl.DyeTable != null ? Mtrl.DyeTable.RowAsBytes(rowIdx) : []; - if (data.Length != row.Length && data.Length != row.Length + dyeRow.Length) + if (!Mtrl.Table.MergeSpecificValues(row, halves, copy)) return false; - data.AsSpan(0, row.Length).TryCopyTo(row); - data.AsSpan(row.Length).TryCopyTo(dyeRow); + Mtrl.DyeTable?.MergeSpecificValues(dyeRow, data.AsSpan(row.Length * 2), dyeCopy); UpdateColorTableRowPreview(rowIdx); - return true; } catch From f679e0cceeba9751f216c7c82734a25c71945da8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 25 Dec 2024 21:58:43 +0100 Subject: [PATCH 495/865] Fix some imgui assertions. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 215e0172..d9caded5 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 215e01722a319c70b271dd23a40d99edc3fc197e +Subproject commit d9caded5efb7c9db0a273a43bb5f6d53cf4ace7f diff --git a/Penumbra.GameData b/Penumbra.GameData index 6848397d..ffc149cc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 6848397dd77cfcdbff1accd860d5b7e95f8c9fe5 +Subproject commit ffc149cc8c169c2c6e838cbd138676f6fe4daeea From b3883c1306ed9b1b0a90699353efb2bf9f1bfdfa Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 26 Dec 2024 00:06:51 +0100 Subject: [PATCH 496/865] Add handling for cached TMBs. --- Penumbra.GameData | 2 +- Penumbra/Communication/ResolvedFileChanged.cs | 4 +- Penumbra/Interop/GameState.cs | 3 + .../Animation/GetCachedScheduleResource.cs | 53 +++++++ .../Interop/Hooks/Animation/LoadActionTmb.cs | 55 +++++++ Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../SchedulerResourceManagementService.cs | 92 ++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 109 +++++++++----- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 135 +++++++++++++++++- 9 files changed, 415 insertions(+), 40 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs create mode 100644 Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs create mode 100644 Penumbra/Interop/Services/SchedulerResourceManagementService.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index ffc149cc..19355cfa 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ffc149cc8c169c2c6e838cbd138676f6fe4daeea +Subproject commit 19355cfa0ec80e8d5a91de11ecffc49257b37b53 diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index 75444340..0c91a18b 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -1,6 +1,5 @@ using OtterGui.Classes; using Penumbra.Collections; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.String.Classes; @@ -33,5 +32,8 @@ public sealed class ResolvedFileChanged() { /// DalamudSubstitutionProvider = 0, + + /// + SchedulerResourceManagementService = 0, } } diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 7e7abcd8..f80ef696 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -49,6 +49,9 @@ public class GameState : IService public void RestoreAnimationData(ResolveData old) => _animationLoadData.Value = old; + public readonly ThreadLocal InLoadActionTmb = new(() => false); + public readonly ThreadLocal SkipTmbCache = new(() => false); + #endregion #region Sound Data diff --git a/Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs b/Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs new file mode 100644 index 00000000..6ce1f899 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/GetCachedScheduleResource.cs @@ -0,0 +1,53 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using JetBrains.Annotations; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.Structs; +using Penumbra.String; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a cached TMB resource from SchedulerResourceManagement. +public sealed unsafe class GetCachedScheduleResource : FastHook +{ + private readonly GameState _state; + + public GetCachedScheduleResource(HookManager hooks, GameState state) + { + _state = state; + Task = hooks.CreateHook("Get Cached Schedule Resource", Sigs.GetCachedScheduleResource, Detour, + !HookOverrides.Instance.Animation.GetCachedScheduleResource); + } + + public delegate SchedulerResource* Delegate(SchedulerResourceManagement* a, ScheduleResourceLoadData* b, byte useMap); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private SchedulerResource* Detour(SchedulerResourceManagement* a, ScheduleResourceLoadData* b, byte c) + { + if (_state.SkipTmbCache.Value) + { + Penumbra.Log.Verbose( + $"[GetCachedScheduleResource] Called with 0x{(ulong)a:X}, {b->Id}, {new CiByteString(b->Path, MetaDataComputation.None)}, {c} from LoadActionTmb with forced skipping of cache, returning NULL."); + return null; + } + + var ret = Task.Result.Original(a, b, c); + Penumbra.Log.Excessive( + $"[GetCachedScheduleResource] Called with 0x{(ulong)a:X}, {b->Id}, {new CiByteString(b->Path, MetaDataComputation.None)}, {c}, returning 0x{(ulong)ret:X} ({(ret != null && Resource(ret) != null ? Resource(ret)->FileName().ToString() : "No Path")})."); + return ret; + } + + public struct ScheduleResourceLoadData + { + [UsedImplicitly] + public byte* Path; + + [UsedImplicitly] + public uint Id; + } + + + // #TODO: remove when fixed in CS. + public static ResourceHandle* Resource(SchedulerResource* r) + => ((ResourceHandle**)r)[3]; +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs b/Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs new file mode 100644 index 00000000..457465d2 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/LoadActionTmb.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.Interop.Services; +using Penumbra.String; + +namespace Penumbra.Interop.Hooks.Animation; + +/// Load a Action TMB. +public sealed unsafe class LoadActionTmb : FastHook +{ + private readonly GameState _state; + private readonly SchedulerResourceManagementService _scheduler; + + public LoadActionTmb(HookManager hooks, GameState state, SchedulerResourceManagementService scheduler) + { + _state = state; + _scheduler = scheduler; + Task = hooks.CreateHook("Load Action TMB", Sigs.LoadActionTmb, Detour, !HookOverrides.Instance.Animation.LoadActionTmb); + } + + public delegate SchedulerResource* Delegate(SchedulerResourceManagement* scheduler, + GetCachedScheduleResource.ScheduleResourceLoadData* loadData, nint b, byte c, byte d, byte e); + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private SchedulerResource* Detour(SchedulerResourceManagement* scheduler, GetCachedScheduleResource.ScheduleResourceLoadData* loadData, + nint b, byte c, byte d, byte e) + { + _state.InLoadActionTmb.Value = true; + SchedulerResource* ret; + if (ShouldSkipCache(loadData)) + { + _state.SkipTmbCache.Value = true; + ret = Task.Result.Original(scheduler, loadData, b, c, d, 1); + Penumbra.Log.Verbose( + $"[LoadActionTMB] Called with 0x{(ulong)scheduler:X}, {loadData->Id}, {new CiByteString(loadData->Path, MetaDataComputation.None)}, 0x{b:X}, {c}, {d}, {e}, forced no-cache use, returned 0x{(ulong)ret:X} ({(ret != null && GetCachedScheduleResource.Resource(ret) != null ? GetCachedScheduleResource.Resource(ret)->FileName().ToString() : "No Path")})."); + _state.SkipTmbCache.Value = false; + } + else + { + ret = Task.Result.Original(scheduler, loadData, b, c, d, e); + Penumbra.Log.Excessive( + $"[LoadActionTMB] Called with 0x{(ulong)scheduler:X}, {loadData->Id}, {new CiByteString(loadData->Path)}, 0x{b:X}, {c}, {d}, {e}, returned 0x{(ulong)ret:X} ({(ret != null && GetCachedScheduleResource.Resource(ret) != null ? GetCachedScheduleResource.Resource(ret)->FileName().ToString() : "No Path")})."); + } + + _state.InLoadActionTmb.Value = false; + + return ret; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool ShouldSkipCache(GetCachedScheduleResource.ScheduleResourceLoadData* loadData) + => _scheduler.Contains(loadData->Id); +} diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 63d93c9d..2aeeb14b 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -43,6 +43,8 @@ public class HookOverrides public bool SomeMountAnimation; public bool SomePapLoad; public bool SomeParasolAnimation; + public bool GetCachedScheduleResource; + public bool LoadActionTmb; } public struct MetaHooks diff --git a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs new file mode 100644 index 00000000..1d56fcdb --- /dev/null +++ b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs @@ -0,0 +1,92 @@ +using System.Collections.Frozen; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; +using Lumina.Excel.Sheets; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.Mods.Editor; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.String.Classes; + +namespace Penumbra.Interop.Services; + +public unsafe class SchedulerResourceManagementService : IService, IDisposable +{ + private static readonly CiByteString TmbExtension = new(".tmb"u8, MetaDataComputation.All); + private static readonly CiByteString FolderPrefix = new("chara/action/"u8, MetaDataComputation.All); + + private readonly CommunicatorService _communicator; + private readonly FrozenDictionary _actionTmbs; + + private readonly ConcurrentDictionary _listedTmbIds = []; + + public bool Contains(uint tmbId) + => _listedTmbIds.ContainsKey(tmbId); + + public IReadOnlyDictionary ListedTmbs + => _listedTmbIds; + + public IReadOnlyDictionary ActionTmbs + => _actionTmbs; + + public SchedulerResourceManagementService(IGameInteropProvider interop, CommunicatorService communicator, IDataManager dataManager) + { + _communicator = communicator; + _actionTmbs = CreateActionTmbs(dataManager); + _communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.SchedulerResourceManagementService); + interop.InitializeFromAttributes(this); + } + + private void OnResolvedFileChange(ModCollection collection, ResolvedFileChanged.Type type, Utf8GamePath gamePath, FullPath oldPath, + FullPath newPath, IMod? mod) + { + switch (type) + { + case ResolvedFileChanged.Type.Added: + CheckFile(gamePath); + return; + case ResolvedFileChanged.Type.FullRecomputeFinished: + foreach (var path in collection.ResolvedFiles.Keys) + CheckFile(path); + return; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void CheckFile(Utf8GamePath gamePath) + { + if (!gamePath.Extension().Equals(TmbExtension)) + return; + + if (!gamePath.Path.StartsWith(FolderPrefix)) + return; + + var tmb = gamePath.Path.Substring(FolderPrefix.Length, gamePath.Length - FolderPrefix.Length - TmbExtension.Length).Clone(); + if (_actionTmbs.TryGetValue(tmb, out var rowId)) + _listedTmbIds[rowId] = tmb; + else + Penumbra.Log.Debug($"Action TMB {gamePath} encountered with no corresponding row ID."); + } + + [Signature(Sigs.SchedulerResourceManagementInstance, ScanType = ScanType.StaticAddress)] + public readonly SchedulerResourceManagement** Address = null; + + public SchedulerResourceManagement* Scheduler + => *Address; + + public void Dispose() + { + _listedTmbIds.Clear(); + _communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange); + } + + private static FrozenDictionary CreateActionTmbs(IDataManager dataManager) + { + var sheet = dataManager.GetExcelSheet(); + return sheet.Where(row => !row.Key.IsEmpty).DistinctBy(row => row.Key).ToFrozenDictionary(row => new CiByteString(row.Key, MetaDataComputation.All).Clone(), row => row.RowId); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 30605101..125dbfa1 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -69,38 +69,39 @@ public class Diagnostics(ServiceManager provider) : IUiService public class DebugTab : Window, ITab, IUiService { - private readonly PerformanceTracker _performance; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly ModManager _modManager; - private readonly ValidityChecker _validityChecker; - private readonly HttpApi _httpApi; - private readonly ActorManager _actors; - private readonly StainService _stains; - private readonly GlobalVariablesDrawer _globalVariablesDrawer; - private readonly ResourceManagerService _resourceManager; - private readonly CollectionResolver _collectionResolver; - private readonly DrawObjectState _drawObjectState; - private readonly PathState _pathState; - private readonly SubfileHelper _subfileHelper; - private readonly IdentifiedCollectionCache _identifiedCollectionCache; - private readonly CutsceneService _cutsceneService; - private readonly ModImportManager _modImporter; - private readonly ImportPopup _importPopup; - private readonly FrameworkManager _framework; - private readonly TextureManager _textureManager; - private readonly ShaderReplacementFixer _shaderReplacementFixer; - private readonly RedrawService _redraws; - private readonly DictEmote _emotes; - private readonly Diagnostics _diagnostics; - private readonly ObjectManager _objects; - private readonly IClientState _clientState; - private readonly IDataManager _dataManager; - private readonly IpcTester _ipcTester; - private readonly CrashHandlerPanel _crashHandlerPanel; - private readonly TexHeaderDrawer _texHeaderDrawer; - private readonly HookOverrideDrawer _hookOverrides; - private readonly RsfService _rsfService; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; + private readonly ValidityChecker _validityChecker; + private readonly HttpApi _httpApi; + private readonly ActorManager _actors; + private readonly StainService _stains; + private readonly GlobalVariablesDrawer _globalVariablesDrawer; + private readonly ResourceManagerService _resourceManager; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + private readonly PathState _pathState; + private readonly SubfileHelper _subfileHelper; + private readonly IdentifiedCollectionCache _identifiedCollectionCache; + private readonly CutsceneService _cutsceneService; + private readonly ModImportManager _modImporter; + private readonly ImportPopup _importPopup; + private readonly FrameworkManager _framework; + private readonly TextureManager _textureManager; + private readonly ShaderReplacementFixer _shaderReplacementFixer; + private readonly RedrawService _redraws; + private readonly DictEmote _emotes; + private readonly Diagnostics _diagnostics; + private readonly ObjectManager _objects; + private readonly IClientState _clientState; + private readonly IDataManager _dataManager; + private readonly IpcTester _ipcTester; + private readonly CrashHandlerPanel _crashHandlerPanel; + private readonly TexHeaderDrawer _texHeaderDrawer; + private readonly HookOverrideDrawer _hookOverrides; + private readonly RsfService _rsfService; + private readonly SchedulerResourceManagementService _schedulerService; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -110,7 +111,8 @@ public class DebugTab : Window, ITab, IUiService CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, - HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer) + HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, + SchedulerResourceManagementService schedulerService) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -148,6 +150,7 @@ public class DebugTab : Window, ITab, IUiService _hookOverrides = hookOverrides; _rsfService = rsfService; _globalVariablesDrawer = globalVariablesDrawer; + _schedulerService = schedulerService; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -672,6 +675,22 @@ public class DebugTab : Window, ITab, IUiService } } } + + using (var tmbCache = TreeNode("TMB Cache")) + { + if (tmbCache) + { + using var table = Table("###TmbTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key)) + { + ImUtf8.DrawTableColumn($"{id:D6}"); + ImUtf8.DrawTableColumn(name.Span); + } + } + } + } } private void DrawData() @@ -680,6 +699,7 @@ public class DebugTab : Window, ITab, IUiService return; DrawEmotes(); + DrawActionTmbs(); DrawStainTemplates(); DrawAtch(); } @@ -739,6 +759,27 @@ public class DebugTab : Window, ITab, IUiService ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); } + private void DrawActionTmbs() + { + using var mainTree = TreeNode("Action TMBs"); + if (!mainTree) + return; + + using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); + if (!table) + return; + + var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); + var dummy = ImGuiClip.ClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + p => + { + ImUtf8.DrawTableColumn($"{p.Value}"); + ImUtf8.DrawTableColumn(p.Key.Span); + }); + ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); + } + private void DrawStainTemplates() { using var mainTree = TreeNode("Staining Templates"); @@ -1061,7 +1102,7 @@ public class DebugTab : Window, ITab, IUiService } ImUtf8.HoverTooltip("Click to copy address to clipboard."u8); - } + } public static unsafe void DrawCopyableAddress(ReadOnlySpan label, nint address) => DrawCopyableAddress(label, (void*)address); diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 601e9b4d..4e6cf62c 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -1,12 +1,23 @@ +using Dalamud.Hooking; 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; +using Penumbra.Interop.Structs; +using Penumbra.String; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.UI.Tabs.Debug; -public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, ResidentResourceManager residentResources) : IUiService +public unsafe class GlobalVariablesDrawer( + CharacterUtility characterUtility, + ResidentResourceManager residentResources, + SchedulerResourceManagementService scheduler) : IUiService { /// Draw information about some game global variables. public void Draw() @@ -16,13 +27,22 @@ public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, Res if (!header) return; - DebugTab.DrawCopyableAddress("CharacterUtility"u8, characterUtility.Address); - DebugTab.DrawCopyableAddress("ResidentResourceManager"u8, residentResources.Address); - DebugTab.DrawCopyableAddress("Device"u8, Device.Instance()); + 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()); DrawCharacterUtility(); DrawResidentResources(); + DrawSchedulerResourcesMap(); + DrawSchedulerResourcesList(); } + /// /// Draw information about the character utility class from SE, /// displaying all files, their sizes, the default files and the default sizes. @@ -123,4 +143,111 @@ public unsafe class GlobalVariablesDrawer(CharacterUtility characterUtility, Res ImUtf8.DrawTableColumn(length.ToString()); } } + + private string _schedulerFilterList = string.Empty; + private string _schedulerFilterMap = string.Empty; + private CiByteString _schedulerFilterListU8 = CiByteString.Empty; + private CiByteString _schedulerFilterMapU8 = CiByteString.Empty; + private int _shownResourcesList = 0; + private int _shownResourcesMap = 0; + + private void DrawSchedulerResourcesMap() + { + using var tree = ImUtf8.TreeNode("Scheduler Resources (Map)"u8); + if (!tree) + return; + + if (scheduler.Address == null || scheduler.Scheduler == null) + return; + + if (ImUtf8.InputText("##SchedulerMapFilter"u8, ref _schedulerFilterMap, "Filter..."u8)) + _schedulerFilterMapU8 = CiByteString.FromString(_schedulerFilterMap, out var t, MetaDataComputation.All, false) + ? t + : CiByteString.Empty; + ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->NumResources}"); + using var table = ImUtf8.Table("##SchedulerMapResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var map = (StdMap>*)&scheduler.Scheduler->Unknown; + var total = 0; + _shownResourcesMap = 0; + foreach (var (key, resourcePtr) in *map) + { + var resource = resourcePtr.Value; + if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMapU8.Span) >= 0) + { + ImUtf8.DrawTableColumn($"[{total:D4}]"); + ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); + ImUtf8.DrawTableColumn($"{resource->Consumers}"); + ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + var resourceHandle = *((ResourceHandle**)resource + 3); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); + ImGui.TableNextColumn(); + uint dataLength = 0; + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + ImUtf8.DrawTableColumn($"{dataLength}"); + ++_shownResourcesMap; + } + + ++total; + } + } + + private void DrawSchedulerResourcesList() + { + using var tree = ImUtf8.TreeNode("Scheduler Resources (List)"u8); + if (!tree) + return; + + if (scheduler.Address == null || scheduler.Scheduler == null) + return; + + if (ImUtf8.InputText("##SchedulerListFilter"u8, ref _schedulerFilterList, "Filter..."u8)) + _schedulerFilterListU8 = CiByteString.FromString(_schedulerFilterList, out var t, MetaDataComputation.All, false) + ? t + : CiByteString.Empty; + ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->NumResources}"); + using var table = ImUtf8.Table("##SchedulerListResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + var resource = scheduler.Scheduler->Begin; + var total = 0; + _shownResourcesList = 0; + while (resource != null && total < (int)scheduler.Scheduler->NumResources) + { + if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) + { + ImUtf8.DrawTableColumn($"[{total:D4}]"); + ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); + ImUtf8.DrawTableColumn($"{resource->Consumers}"); + ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + var resourceHandle = *((ResourceHandle**)resource + 3); + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resourceHandle:X}"); + ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); + ImGui.TableNextColumn(); + uint dataLength = 0; + ImUtf8.CopyOnClickSelectable($"0x{(ulong)resource->GetResourceData(&dataLength):X}"); + ImUtf8.DrawTableColumn($"{dataLength}"); + ++_shownResourcesList; + } + + resource = resource->Previous; + ++total; + } + } } From f24056ea3101a4ead083ea26a27caab23d77fb8a Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 25 Dec 2024 23:08:52 +0000 Subject: [PATCH 497/865] [CI] Updating repo.json for testing_1.3.2.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c0e561da..97e55af0 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.2.0", - "TestingAssemblyVersion": "1.3.2.0", + "TestingAssemblyVersion": "1.3.2.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 25d0a2c9a83323336bbff7865e3cd054b5488075 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 29 Dec 2024 23:04:32 +0100 Subject: [PATCH 498/865] Fix issue with ring IMCs in resource tree. --- Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index 0c36b745..bdf66a16 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -161,7 +161,7 @@ internal partial record ResolveContext return variant.Id; } - var entry = ImcFile.GetEntry(imcFileData, Slot.ToSlot(), variant, out var exists); + var entry = ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), variant, out var exists); if (!exists) return variant.Id; From 0e2364497f2b16fbfd23aa0b6d9bfac9825a1da7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 30 Dec 2024 00:33:46 +0100 Subject: [PATCH 499/865] Maybe fix mtrl file issues. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 19355cfa..33de79bc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 19355cfa0ec80e8d5a91de11ecffc49257b37b53 +Subproject commit 33de79bc62eb014298856ed5c6b6edbe819db26c From 2483f3dcdf776a1255b9400f1d5f26ea719cc5e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:08:10 +0100 Subject: [PATCH 500/865] Add Temporary Settings class --- Penumbra/Mods/Settings/TemporaryModSettings.cs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Penumbra/Mods/Settings/TemporaryModSettings.cs diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs new file mode 100644 index 00000000..a0cdc2bb --- /dev/null +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -0,0 +1,8 @@ +namespace Penumbra.Mods.Settings; + +public sealed class TemporaryModSettings : ModSettings +{ + public string Source = string.Empty; + public int Lock = 0; + public bool ForceInherit; +} From 50b5eeb700fab8d40880a8fa1839cfe598e0b5bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:08:35 +0100 Subject: [PATCH 501/865] Add FullModSettings struct. --- Penumbra/Mods/Settings/FullModSettings.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Penumbra/Mods/Settings/FullModSettings.cs diff --git a/Penumbra/Mods/Settings/FullModSettings.cs b/Penumbra/Mods/Settings/FullModSettings.cs new file mode 100644 index 00000000..904b56bd --- /dev/null +++ b/Penumbra/Mods/Settings/FullModSettings.cs @@ -0,0 +1,19 @@ +namespace Penumbra.Mods.Settings; + +public readonly record struct FullModSettings(ModSettings? Settings = null, TemporaryModSettings? TempSettings = null) +{ + public static readonly FullModSettings Empty = new(); + + public ModSettings? Resolve() + { + if (TempSettings == null) + return Settings; + if (TempSettings.ForceInherit) + return null; + + return TempSettings; + } + + public FullModSettings DeepCopy() + => new(Settings?.DeepCopy()); +} From 7a2691b9429cf801981389207ef563c5240b004a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:23:29 +0100 Subject: [PATCH 502/865] Add colors for temporary settings. --- Penumbra/UI/Classes/Colors.cs | 64 ++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index d135e10c..0389730d 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -31,6 +31,10 @@ public enum ColorId ResTreeNonNetworked, PredefinedTagAdd, PredefinedTagRemove, + TemporaryEnabledMod, + TemporaryDisabledMod, + TemporaryInheritedMod, + TemporaryInheritedDisabledMod, } public static class Colors @@ -52,34 +56,38 @@ public static class Colors => color switch { // @formatter:off - ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), - ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), - ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), - ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), - ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), - ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), - ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), - ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), - ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), - ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), - ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), - ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), - ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), - ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), - ColorId.SelectedCollection => ( 0x6069C056, "Currently Selected Collection", "The collection that is currently selected and being edited."), - ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."), - ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), - ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), - ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), - ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), - ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), - ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), - ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), - ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), - ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), - ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), - ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), - _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), + ColorId.EnabledMod => ( 0xFFFFFFFF, "Enabled Mod", "A mod that is enabled by the currently selected collection." ), + ColorId.DisabledMod => ( 0xFF686880, "Disabled Mod", "A mod that is disabled by the currently selected collection." ), + ColorId.UndefinedMod => ( 0xFF808080, "Mod With No Settings", "A mod that is not configured in the currently selected collection or any of the collections it inherits from, and thus implicitly disabled." ), + ColorId.InheritedMod => ( 0xFFD0FFFF, "Mod Enabled By Inheritance", "A mod that is not configured in the currently selected collection, but enabled in a collection it inherits from." ), + ColorId.InheritedDisabledMod => ( 0xFF688080, "Mod Disabled By Inheritance", "A mod that is not configured in the currently selected collection, but disabled in a collection it inherits from."), + ColorId.NewMod => ( 0xFF66DD66, "New Mod", "A mod that was newly imported or created during this session and has not been enabled yet." ), + ColorId.ConflictingMod => ( 0xFFAAAAFF, "Mod With Unresolved Conflicts", "An enabled mod that has conflicts with another enabled mod on the same priority level." ), + ColorId.HandledConflictMod => ( 0xFFD0FFD0, "Mod With Resolved Conflicts", "An enabled mod that has conflicts with another enabled mod on a different priority level." ), + ColorId.FolderExpanded => ( 0xFFFFF0C0, "Expanded Mod Folder", "A mod folder that is currently expanded." ), + ColorId.FolderCollapsed => ( 0xFFFFF0C0, "Collapsed Mod Folder", "A mod folder that is currently collapsed." ), + ColorId.FolderLine => ( 0xFFFFF0C0, "Expanded Mod Folder Line", "The line signifying which descendants belong to an expanded mod folder." ), + ColorId.ItemId => ( 0xFF808080, "Item Id", "The numeric model id of the given item to the right of changed items." ), + ColorId.IncreasedMetaValue => ( 0x80008000, "Increased Meta Manipulation Value", "An increased meta manipulation value for floats or an enabled toggle where the default is disabled."), + ColorId.DecreasedMetaValue => ( 0x80000080, "Decreased Meta Manipulation Value", "A decreased meta manipulation value for floats or a disabled toggle where the default is enabled."), + ColorId.SelectedCollection => ( 0x6069C056, "Currently Selected Collection", "The collection that is currently selected and being edited."), + ColorId.RedundantAssignment => ( 0x6050D0D0, "Redundant Collection Assignment", "A collection assignment that currently has no effect as it is redundant with more general assignments."), + ColorId.NoModsAssignment => ( 0x50000080, "'Use No Mods' Collection Assignment", "A collection assignment set to not use any mods at all."), + ColorId.NoAssignment => ( 0x00000000, "Unassigned Collection Assignment", "A collection assignment that is not configured to any collection and thus just has no specific treatment."), + ColorId.SelectorPriority => ( 0xFF808080, "Mod Selector Priority", "The priority displayed for non-zero priority mods in the mod selector."), + ColorId.InGameHighlight => ( 0xFFEBCF89, "In-Game Highlight (Primary)", "An in-game element that has been highlighted for ease of editing."), + ColorId.InGameHighlight2 => ( 0xFF446CC0, "In-Game Highlight (Secondary)", "Another in-game element that has been highlighted for ease of editing."), + ColorId.ResTreeLocalPlayer => ( 0xFFFFE0A0, "On-Screen: You", "You and what you own (mount, minion, accessory, pets and so on), in the On-Screen tab." ), + ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), + ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), + ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), + ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), + ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), + ColorId.TemporaryEnabledMod => ( 0xFFFFC0A0, "Mod Enabled By Temporary Settings", "A mod that is enabled by temporary settings in the currently selected collection." ), + ColorId.TemporaryDisabledMod => ( 0xFFB08070, "Mod Disabled By Temporary Settings", "A mod that is disabled by temporary settings in the currently selected collection." ), + ColorId.TemporaryInheritedMod => ( 0xFFE8FFB0, "Mod Enabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), + ColorId.TemporaryInheritedDisabledMod => ( 0xFF90A080, "Mod Disabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), + _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; From fbbfe5e00d8b53b2e103c6bf29f7ea870f348b0a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 10:39:24 +0100 Subject: [PATCH 503/865] Extract collection counters. --- Penumbra/Collections/Cache/AtchCache.cs | 5 ++-- Penumbra/Collections/Cache/CollectionCache.cs | 8 +++--- .../Cache/CollectionCacheManager.cs | 4 +-- Penumbra/Collections/Cache/ImcCache.cs | 4 +-- Penumbra/Collections/CollectionCounters.cs | 28 +++++++++++++++++++ .../Collections/Manager/CollectionStorage.cs | 2 +- .../Manager/TempCollectionManager.cs | 2 +- Penumbra/Collections/ModCollection.cs | 15 ++-------- .../Interop/PathResolving/PathDataHandler.cs | 8 +++--- Penumbra/UI/Tabs/Debug/DebugTab.cs | 4 +-- 10 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 Penumbra/Collections/CollectionCounters.cs diff --git a/Penumbra/Collections/Cache/AtchCache.cs b/Penumbra/Collections/Cache/AtchCache.cs index 9e0f6caf..10990553 100644 --- a/Penumbra/Collections/Cache/AtchCache.cs +++ b/Penumbra/Collections/Cache/AtchCache.cs @@ -2,7 +2,6 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Collections.Cache; @@ -37,7 +36,7 @@ public sealed class AtchCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(AtchIdentifier identifier, AtchEntry entry) { - ++Collection.AtchChangeCounter; + Collection.Counters.IncrementAtch(); ApplyFile(identifier, entry); } @@ -68,7 +67,7 @@ public sealed class AtchCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(AtchIdentifier identifier) { - ++Collection.AtchChangeCounter; + Collection.Counters.IncrementAtch(); if (!_atchFiles.TryGetValue(identifier.GenderRace, out var pair)) return; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 64cf54ea..bc431e88 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -177,7 +177,7 @@ public sealed class CollectionCache : IDisposable var (paths, manipulations) = ModData.RemoveMod(mod); if (addMetaChanges) - _collection.IncrementCounter(); + _collection.Counters.IncrementChange(); foreach (var path in paths) { @@ -250,7 +250,7 @@ public sealed class CollectionCache : IDisposable if (addMetaChanges) { - _collection.IncrementCounter(); + _collection.Counters.IncrementChange(); _manager.MetaFileManager.ApplyDefaultFiles(_collection); } } @@ -408,12 +408,12 @@ public sealed class CollectionCache : IDisposable // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { - if (_changedItemsSaveCounter == _collection.ChangeCounter) + if (_changedItemsSaveCounter == _collection.Counters.Change) return; try { - _changedItemsSaveCounter = _collection.ChangeCounter; + _changedItemsSaveCounter = _collection.Counters.Change; _changedItems.Clear(); // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index a3b6bb83..c3e00502 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -187,7 +187,7 @@ public class CollectionCacheManager : IDisposable, IService foreach (var mod in _modStorage) cache.AddModSync(mod, false); - collection.IncrementCounter(); + collection.Counters.IncrementChange(); MetaFileManager.ApplyDefaultFiles(collection); ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeFinished, Utf8GamePath.Empty, FullPath.Empty, @@ -297,7 +297,7 @@ public class CollectionCacheManager : IDisposable, IService private void IncrementCounters() { foreach (var collection in _storage.Where(c => c.HasCache)) - collection.IncrementCounter(); + collection.Counters.IncrementChange(); MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; } diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index cac52f99..0f610d90 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -39,7 +39,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ImcIdentifier identifier, ImcEntry entry) { - ++Collection.ImcChangeCounter; + Collection.Counters.IncrementImc(); ApplyFile(identifier, entry); } @@ -71,7 +71,7 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) protected override void RevertModInternal(ImcIdentifier identifier) { - ++Collection.ImcChangeCounter; + Collection.Counters.IncrementImc(); var path = identifier.GamePath().Path; if (!_imcFiles.TryGetValue(path, out var pair)) return; diff --git a/Penumbra/Collections/CollectionCounters.cs b/Penumbra/Collections/CollectionCounters.cs new file mode 100644 index 00000000..91d240d6 --- /dev/null +++ b/Penumbra/Collections/CollectionCounters.cs @@ -0,0 +1,28 @@ +namespace Penumbra.Collections; + +public struct CollectionCounters(int changeCounter) +{ + /// Count the number of changes of the effective file list. + public int Change { get; private set; } = changeCounter; + + /// Count the number of IMC-relevant changes of the effective file list. + public int Imc { get; private set; } + + /// Count the number of ATCH-relevant changes of the effective file list. + public int Atch { get; private set; } + + /// Increment the number of changes in the effective file list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IncrementChange() + => ++Change; + + /// Increment the number of IMC-relevant changes in the effective file list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IncrementImc() + => ++Imc; + + /// Increment the number of ATCH-relevant changes in the effective file list. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int IncrementAtch() + => ++Imc; +} diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index a326fb92..cdbe11dc 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -372,7 +372,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { var (settings, _) = collection[mod.Index]; if (settings is { Enabled: true }) - collection.IncrementCounter(); + collection.Counters.IncrementChange(); } } } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 5c893232..e5b844c8 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -75,7 +75,7 @@ public class TempCollectionManager : IDisposable, IService _storage.Delete(collection); Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); - GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0); + GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { if (Collections[i].Collection != collection) diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index db9c19cb..95e78da0 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -50,18 +50,7 @@ public partial class ModCollection /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } - /// - /// Count the number of changes of the effective file list. - /// This is used for material and imc changes. - /// - public int ChangeCounter { get; private set; } - - public uint ImcChangeCounter { get; set; } - public uint AtchChangeCounter { get; set; } - - /// Increment the number of changes in the effective file list. - public int IncrementCounter() - => ++ChangeCounter; + public CollectionCounters Counters; /// /// If a ModSetting is null, it can be inherited from other collections. @@ -213,7 +202,7 @@ public partial class ModCollection Id = id; LocalId = localId; Index = index; - ChangeCounter = changeCounter; + Counters = new CollectionCounters(changeCounter); Settings = appliedSettings; UnusedSettings = settings; DirectlyInheritsFrom = inheritsFrom; diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 5439151f..25d4f7ea 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.ImcChangeCounter}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Imc}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -47,17 +47,17 @@ public static class PathDataHandler /// Create the encoding path for an ATCH file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateAtch(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.AtchChangeCounter}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Atch}_{DiscriminatorString}|{path}"); /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) - => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); /// The base function shared by most file types. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static FullPath CreateBase(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.ChangeCounter}_{DiscriminatorString}|{path}"); + => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{DiscriminatorString}|{path}"); /// Read an additional data blurb and parse it into usable data for all file types but Materials. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 125dbfa1..95afb10f 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -204,7 +204,7 @@ public class DebugTab : Window, ITab, IUiService if (collection.HasCache) { using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode($"{collection.Name} (Change Counter {collection.ChangeCounter})###{collection.Name}"); + using var node = TreeNode($"{collection.Name} (Change Counter {collection.Counters.Change})###{collection.Name}"); if (!node) continue; @@ -239,7 +239,7 @@ public class DebugTab : Window, ITab, IUiService else { using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})", + TreeNode($"{collection.AnonymizedName} (Change Counter {collection.Counters.Change})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } From 67305d507a4e496202a66096050a9ffadedc5f52 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 11:36:30 +0100 Subject: [PATCH 504/865] Extract ModCollectionIdentity. --- OtterGui | 2 +- Penumbra/Api/Api/CollectionApi.cs | 32 +++++----- Penumbra/Api/Api/GameStateApi.cs | 4 +- Penumbra/Api/Api/ModSettingsApi.cs | 6 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 6 +- Penumbra/Api/TempModManager.cs | 4 +- Penumbra/Collections/Cache/CollectionCache.cs | 2 +- .../Cache/CollectionCacheManager.cs | 36 +++++------ .../Collections/CollectionAutoSelector.cs | 2 +- .../Manager/ActiveCollectionMigration.cs | 2 +- .../Collections/Manager/ActiveCollections.cs | 46 +++++++------- .../Collections/Manager/CollectionStorage.cs | 36 +++++------ .../Manager/IndividualCollections.Files.cs | 10 +-- .../Collections/Manager/InheritanceManager.cs | 14 ++--- .../Manager/TempCollectionManager.cs | 16 ++--- Penumbra/Collections/ModCollection.cs | 63 ++++++------------- Penumbra/Collections/ModCollectionIdentity.cs | 42 +++++++++++++ Penumbra/Collections/ModCollectionSave.cs | 16 ++--- Penumbra/Collections/ResolveData.cs | 2 +- Penumbra/CommandHandler.cs | 22 +++---- .../Interop/Hooks/Meta/AtchCallerHook1.cs | 2 +- .../Interop/Hooks/Meta/AtchCallerHook2.cs | 2 +- Penumbra/Interop/PathResolving/MetaState.cs | 2 +- .../Interop/PathResolving/PathDataHandler.cs | 8 +-- .../Processing/ImcFilePostProcessor.cs | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 2 +- Penumbra/Mods/TemporaryMod.cs | 10 +-- Penumbra/Penumbra.cs | 12 ++-- Penumbra/Services/ConfigMigrationService.cs | 8 +-- Penumbra/Services/CrashHandlerService.cs | 4 +- Penumbra/Services/FilenameService.cs | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 25 ++++---- Penumbra/UI/CollectionTab/CollectionCombo.cs | 4 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 16 ++--- .../UI/CollectionTab/CollectionSelector.cs | 6 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 10 +-- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 6 +- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 2 +- .../ResourceWatcher/ResourceWatcherTable.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 22 +++---- Penumbra/UI/Tabs/ModsTab.cs | 4 +- Penumbra/UI/TutorialService.cs | 6 +- 43 files changed, 270 insertions(+), 252 deletions(-) create mode 100644 Penumbra/Collections/ModCollectionIdentity.cs diff --git a/OtterGui b/OtterGui index d9caded5..fcc96daa 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d9caded5efb7c9db0a273a43bb5f6d53cf4ace7f +Subproject commit fcc96daa02633f673325c14aeea6b6b568924f1e diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index 04299187..964da1a5 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -8,7 +8,7 @@ namespace Penumbra.Api.Api; public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : IPenumbraApiCollection, IApiService { public Dictionary GetCollections() - => collections.Storage.ToDictionary(c => c.Id, c => c.Name); + => collections.Storage.ToDictionary(c => c.Identity.Id, c => c.Identity.Name); public List<(Guid Id, string Name)> GetCollectionsByIdentifier(string identifier) { @@ -17,14 +17,14 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : var list = new List<(Guid Id, string Name)>(4); if (Guid.TryParse(identifier, out var guid) && collections.Storage.ById(guid, out var collection) && collection != ModCollection.Empty) - list.Add((collection.Id, collection.Name)); + list.Add((collection.Identity.Id, collection.Identity.Name)); else if (identifier.Length >= 8) - list.AddRange(collections.Storage.Where(c => c.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase)) - .Select(c => (c.Id, c.Name))); + list.AddRange(collections.Storage.Where(c => c.Identity.Identifier.StartsWith(identifier, StringComparison.OrdinalIgnoreCase)) + .Select(c => (c.Identity.Id, c.Identity.Name))); list.AddRange(collections.Storage - .Where(c => string.Equals(c.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Id, c.Name))) - .Select(c => (c.Id, c.Name))); + .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Identity.Id, c.Identity.Name))) + .Select(c => (c.Identity.Id, c.Identity.Name))); return list; } @@ -54,7 +54,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : return null; var collection = collections.Active.ByType((CollectionType)type); - return collection == null ? null : (collection.Id, collection.Name); + return collection == null ? null : (collection.Identity.Id, collection.Identity.Name); } internal (Guid Id, string Name)? GetCollection(byte type) @@ -64,17 +64,17 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : { var id = helpers.AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (false, false, (collections.Active.Default.Id, collections.Active.Default.Name)); + return (false, false, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name)); if (collections.Active.Individuals.TryGetValue(id, out var collection)) - return (true, true, (collection.Id, collection.Name)); + return (true, true, (collection.Identity.Id, collection.Identity.Name)); helpers.AssociatedCollection(gameObjectIdx, out collection); - return (true, false, (collection.Id, collection.Name)); + return (true, false, (collection.Identity.Id, collection.Identity.Name)); } public Guid[] GetCollectionByName(string name) - => collections.Storage.Where(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Id).ToArray(); + => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id).ToArray(); public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, bool allowCreateNew, bool allowDelete) @@ -83,7 +83,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : return (PenumbraApiEc.InvalidArgument, null); var oldCollection = collections.Active.ByType((CollectionType)type); - var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple?(); if (collectionId == null) { if (old == null) @@ -106,7 +106,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : collections.Active.CreateSpecialCollection((CollectionType)type); } - else if (old.Value.Item1 == collection.Id) + else if (old.Value.Item1 == collection.Identity.Id) { return (PenumbraApiEc.NothingChanged, old); } @@ -120,10 +120,10 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : { var id = helpers.AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Id, collections.Active.Default.Name)); + return (PenumbraApiEc.InvalidIdentifier, (collections.Active.Default.Identity.Id, collections.Active.Default.Identity.Name)); var oldCollection = collections.Active.Individuals.TryGetValue(id, out var c) ? c : null; - var old = oldCollection != null ? (oldCollection.Id, oldCollection.Name) : new ValueTuple?(); + var old = oldCollection != null ? (oldCollection.Identity.Id, oldCollection.Identity.Name) : new ValueTuple?(); if (collectionId == null) { if (old == null) @@ -148,7 +148,7 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : var ids = collections.Active.Individuals.GetGroup(id); collections.Active.CreateIndividualCollection(ids); } - else if (old.Value.Item1 == collection.Id) + else if (old.Value.Item1 == collection.Identity.Id) { return (PenumbraApiEc.NothingChanged, old); } diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index c2cae32b..7f70c6bf 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -61,7 +61,7 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public unsafe (nint GameObject, (Guid Id, string Name) Collection) GetDrawObjectInfo(nint drawObject) { var data = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true); - return (data.AssociatedGameObject, (data.ModCollection.Id, data.ModCollection.Name)); + return (data.AssociatedGameObject, (Id: data.ModCollection.Identity.Id, Name: data.ModCollection.Identity.Name)); } public int GetCutsceneParentIndex(int actorIdx) @@ -93,5 +93,5 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable } private void OnCreatedCharacterBase(nint gameObject, ModCollection collection, nint drawObject) - => CreatedCharacterBase?.Invoke(gameObject, collection.Id, drawObject); + => CreatedCharacterBase?.Invoke(gameObject, collection.Identity.Id, drawObject); } diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 8c34c249..3dc900fc 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -77,7 +77,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_collectionManager.Storage.ById(collectionId, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - var settings = collection.Id == Guid.Empty + var settings = collection.Identity.Id == Guid.Empty ? null : ignoreInheritance ? collection.Settings[mod.Index] @@ -217,7 +217,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var collection = _collectionResolver.PlayerCollection(); var (settings, parent) = collection[mod.Index]; if (settings is { Enabled: true }) - ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Id, mod.Identifier, parent != collection); + ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection); } private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) @@ -227,7 +227,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); + => ModSettingChanged?.Invoke(type, collection.Identity.Id, mod?.ModPath.Name ?? string.Empty, inherited); private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index f6d1c9eb..f3c23831 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -146,10 +146,10 @@ public class TemporaryIpcTester( using (ImRaii.PushFont(UiBuilder.MonoFont)) { ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(collection.Identifier); + ImGuiUtil.CopyOnClickSelectable(collection.Identity.Identifier); } - ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.Identity.Name); ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString()); ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0"); ImGuiUtil.DrawTableColumn(string.Join(", ", @@ -199,7 +199,7 @@ public class TemporaryIpcTester( { PrintList("All", tempMods.ModsForAllCollections); foreach (var (collection, list) in tempMods.Mods) - PrintList(collection.Name, list); + PrintList(collection.Identity.Name, list); } } } diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 0b52e64a..b3c6066a 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -85,13 +85,13 @@ public class TempModManager : IDisposable, IService { if (removed) { - Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Removing temporary Mod {mod.Name} from {collection.Identity.AnonymizedName}."); collection.Remove(mod); _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.False, 0, false); } else { - Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Adding {(created ? "new " : string.Empty)}temporary Mod {mod.Name} to {collection.Identity.AnonymizedName}."); collection.Apply(mod, created); _communicator.ModSettingChanged.Invoke(collection, ModSettingChange.TemporaryMod, null, Setting.True, 0, false); } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index bc431e88..ad902aac 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -31,7 +31,7 @@ public sealed class CollectionCache : IDisposable public int Calculating = -1; public string AnonymizedName - => _collection.AnonymizedName; + => _collection.Identity.AnonymizedName; public IEnumerable> AllConflicts => ConflictDict.Values; diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c3e00502..0a851154 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -114,16 +114,16 @@ public class CollectionCacheManager : IDisposable, IService /// Only creates a new cache, does not update an existing one. public bool CreateCache(ModCollection collection) { - if (collection.Index == ModCollection.Empty.Index) + if (collection.Identity.Index == ModCollection.Empty.Identity.Index) return false; if (collection._cache != null) return false; collection._cache = new CollectionCache(this, collection); - if (collection.Index > 0) + if (collection.Identity.Index > 0) Interlocked.Increment(ref _count); - Penumbra.Log.Verbose($"Created new cache for collection {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}."); return true; } @@ -132,32 +132,32 @@ public class CollectionCacheManager : IDisposable, IService /// Does not create caches. /// public void CalculateEffectiveFileList(ModCollection collection) - => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identifier, + => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Identity.Identifier, () => CalculateEffectiveFileListInternal(collection)); private void CalculateEffectiveFileListInternal(ModCollection collection) { // Skip the empty collection. - if (collection.Index == 0) + if (collection.Identity.Index == 0) return; - Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName}"); + Penumbra.Log.Debug($"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName}"); if (!collection.HasCache) { Penumbra.Log.Error( - $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, no cache exists."); + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists."); } else if (collection._cache!.Calculating != -1) { Penumbra.Log.Error( - $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); } else { FullRecalculation(collection); Penumbra.Log.Debug( - $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.AnonymizedName} finished."); + $"[{Environment.CurrentManagedThreadId}] Recalculation of effective file list for {collection.Identity.AnonymizedName} finished."); } } @@ -213,7 +213,7 @@ public class CollectionCacheManager : IDisposable, IService else { RemoveCache(old); - if (type is not CollectionType.Inactive && newCollection != null && newCollection.Index != 0 && CreateCache(newCollection)) + if (type is not CollectionType.Inactive && newCollection != null && newCollection.Identity.Index != 0 && CreateCache(newCollection)) CalculateEffectiveFileList(newCollection); if (type is CollectionType.Default) @@ -258,12 +258,12 @@ public class CollectionCacheManager : IDisposable, IService private void RemoveCache(ModCollection? collection) { if (collection != null - && collection.Index > ModCollection.Empty.Index - && collection.Index != _active.Default.Index - && collection.Index != _active.Interface.Index - && collection.Index != _active.Current.Index - && _active.SpecialAssignments.All(c => c.Value.Index != collection.Index) - && _active.Individuals.All(c => c.Collection.Index != collection.Index)) + && collection.Identity.Index > ModCollection.Empty.Identity.Index + && collection.Identity.Index != _active.Default.Identity.Index + && collection.Identity.Index != _active.Interface.Identity.Index + && collection.Identity.Index != _active.Current.Identity.Index + && _active.SpecialAssignments.All(c => c.Value.Identity.Index != collection.Identity.Index) + && _active.Individuals.All(c => c.Collection.Identity.Index != collection.Identity.Index)) ClearCache(collection); } @@ -359,9 +359,9 @@ public class CollectionCacheManager : IDisposable, IService collection._cache!.Dispose(); collection._cache = null; - if (collection.Index > 0) + if (collection.Identity.Index > 0) Interlocked.Decrement(ref _count); - Penumbra.Log.Verbose($"Cleared cache of collection {collection.AnonymizedName}."); + Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}."); } /// diff --git a/Penumbra/Collections/CollectionAutoSelector.cs b/Penumbra/Collections/CollectionAutoSelector.cs index e24fa6a9..68dac914 100644 --- a/Penumbra/Collections/CollectionAutoSelector.cs +++ b/Penumbra/Collections/CollectionAutoSelector.cs @@ -59,7 +59,7 @@ public sealed class CollectionAutoSelector : IService, IDisposable return; var collection = _resolver.PlayerCollection(); - Penumbra.Log.Debug($"Setting current collection to {collection.Identifier} through automatic collection selection."); + Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection."); _collections.SetCollection(collection, CollectionType.Current); } diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs index 19f781fc..b4af0998 100644 --- a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -48,7 +48,7 @@ public static class ActiveCollectionMigration if (!storage.ByName(collectionName, out var collection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", NotificationType.Warning); + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); dict.Add(player, ModCollection.Empty); } else diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 60f9a427..07fcb430 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -219,7 +219,7 @@ public class ActiveCollections : ISavable, IDisposable, IService _ => null, }; - if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count) + if (oldCollection == null || collection == oldCollection || collection.Identity.Index >= _storage.Count) return; switch (collectionType) @@ -262,13 +262,13 @@ public class ActiveCollections : ISavable, IDisposable, IService var jObj = new JObject { { nameof(Version), Version }, - { nameof(Default), Default.Id }, - { nameof(Interface), Interface.Id }, - { nameof(Current), Current.Id }, + { nameof(Default), Default.Identity.Id }, + { nameof(Interface), Interface.Identity.Id }, + { nameof(Current), Current.Identity.Id }, }; foreach (var (type, collection) in SpecialCollections.WithIndex().Where(p => p.Value != null) .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Id); + jObj.Add(type.ToString(), collection.Identity.Id); jObj.Add(nameof(Individuals), Individuals.ToJObject()); using var j = new JsonTextWriter(writer); @@ -300,7 +300,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (oldCollection == Interface) SetCollection(ModCollection.Empty, CollectionType.Interface); if (oldCollection == Current) - SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current); + SetCollection(Default.Identity.Index > ModCollection.Empty.Identity.Index ? Default : _storage.DefaultNamed, CollectionType.Current); for (var i = 0; i < SpecialCollections.Length; ++i) { @@ -325,11 +325,11 @@ public class ActiveCollections : ISavable, IDisposable, IService { var configChanged = false; // Load the default collection. If the name does not exist take the empty collection. - var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Name; + var defaultName = jObject[nameof(Default)]?.ToObject() ?? ModCollection.Empty.Identity.Name; if (!_storage.ByName(defaultName, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; @@ -340,11 +340,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the interface collection. If no string is set, use the name of whatever was set as Default. - var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Identity.Name; if (!_storage.ByName(interfaceName, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; @@ -355,11 +355,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the current collection. - var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name; + var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Identity.Name; if (!_storage.ByName(currentName, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; @@ -404,7 +404,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ById(defaultId, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; @@ -415,11 +415,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the interface collection. If no string is set, use the name of whatever was set as Default. - var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Id; + var interfaceId = jObject[nameof(Interface)]?.ToObject() ?? Default.Identity.Id; if (!_storage.ById(interfaceId, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Name}.", + $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; @@ -430,11 +430,11 @@ public class ActiveCollections : ISavable, IDisposable, IService } // Load the current collection. - var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Id; + var currentId = jObject[nameof(Current)]?.ToObject() ?? _storage.DefaultNamed.Identity.Id; if (!_storage.ById(currentId, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollection.DefaultCollectionName}.", + $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; @@ -587,7 +587,7 @@ public class ActiveCollections : ISavable, IDisposable, IService case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: { var global = ByType(CollectionType.Individual, _actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); - return global?.Index == checkAssignment.Index + return (global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." : string.Empty; } @@ -596,12 +596,12 @@ public class ActiveCollections : ISavable, IDisposable, IService { var global = ByType(CollectionType.Individual, _actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); - if (global?.Index == checkAssignment.Index) + if ((global != null ? global.Identity.Index : null) == checkAssignment.Identity.Index) return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; } var unowned = ByType(CollectionType.Individual, _actors.CreateNpc(id.Kind, id.DataId)); - return unowned?.Index == checkAssignment.Index + return (unowned != null ? unowned.Identity.Index : null) == checkAssignment.Identity.Index ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." : string.Empty; } @@ -617,7 +617,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (maleNpc == null) { maleNpc = Default; - if (maleNpc.Index != checkAssignment.Index) + if (maleNpc.Identity.Index != checkAssignment.Identity.Index) return string.Empty; collection1 = CollectionType.Default; @@ -626,7 +626,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (femaleNpc == null) { femaleNpc = Default; - if (femaleNpc.Index != checkAssignment.Index) + if (femaleNpc.Identity.Index != checkAssignment.Identity.Index) return string.Empty; collection2 = CollectionType.Default; @@ -646,7 +646,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (assignment == null) continue; - if (assignment.Index == checkAssignment.Index) + if (assignment.Identity.Index == checkAssignment.Identity.Index) return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index cdbe11dc..2ed395ae 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -41,7 +41,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, IReadOnlyList inheritances) { - var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, id, name, CurrentCollectionId, version, Count, allSettings, + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); _collectionsByLocal[CurrentCollectionId] = newCollection; CurrentCollectionId += 1; @@ -57,7 +57,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer } public void Delete(ModCollection collection) - => _collectionsByLocal.Remove(collection.LocalId); + => _collectionsByLocal.Remove(collection.Identity.LocalId); /// The empty collection is always available at Index 0. private readonly List _collections = @@ -92,7 +92,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) { if (name.Length != 0) - return _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); collection = ModCollection.Empty; return true; @@ -102,7 +102,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) { if (id != Guid.Empty) - return _collections.FindFirst(c => c.Id == id, out collection); + return _collections.FindFirst(c => c.Identity.Id == id, out collection); collection = ModCollection.Empty; return true; @@ -158,7 +158,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer var newCollection = Create(name, _collections.Count, duplicate); _collections.Add(newCollection); _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); - Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", NotificationType.Success, false); + Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); return true; } @@ -168,13 +168,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// public bool RemoveCollection(ModCollection collection) { - if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count) + if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) { Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); return false; } - if (collection.Index == DefaultNamed.Index) + if (collection.Identity.Index == DefaultNamed.Identity.Index) { Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); return false; @@ -182,13 +182,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer Delete(collection); _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); - _collections.RemoveAt(collection.Index); + _collections.RemoveAt(collection.Identity.Index); // Update indices. - for (var i = collection.Index; i < Count; ++i) - _collections[i].Index = i; - _collectionsByLocal.Remove(collection.LocalId); + for (var i = collection.Identity.Index; i < Count; ++i) + _collections[i].Identity.Index = i; + _collectionsByLocal.Remove(collection.Identity.LocalId); - Penumbra.Messager.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", NotificationType.Success, false); + Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); return true; } @@ -246,13 +246,13 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { File.Move(file.FullName, correctName, false); Penumbra.Messager.NotificationMessage( - $"Collection {file.Name} does not correspond to {collection.Identifier}, renamed.", + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.", NotificationType.Warning); } catch (Exception ex) { Penumbra.Messager.NotificationMessage( - $"Collection {file.Name} does not correspond to {collection.Identifier}, rename failed:\n{ex}", + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}", NotificationType.Warning); } } @@ -273,7 +273,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer catch (Exception e) { Penumbra.Messager.NotificationMessage(e, - $"Collection {file.Name} does not correspond to {collection.Identifier}, but could not rename.", + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.", NotificationType.Error); } @@ -291,14 +291,14 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// private ModCollection SetDefaultNamedCollection() { - if (ByName(ModCollection.DefaultCollectionName, out var collection)) + if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection)) return collection; - if (AddCollection(ModCollection.DefaultCollectionName, null)) + if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null)) return _collections[^1]; Penumbra.Messager.NotificationMessage( - $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", + $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.", NotificationType.Error); return Count > 1 ? _collections[1] : _collections[0]; } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index f7a26384..60e9fc5f 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -18,7 +18,7 @@ public partial class IndividualCollections foreach (var (name, identifiers, collection) in Assignments) { var tmp = identifiers[0].ToJson(); - tmp.Add("Collection", collection.Id); + tmp.Add("Collection", collection.Identity.Id); tmp.Add("Display", name); ret.Add(tmp); } @@ -182,7 +182,7 @@ public partial class IndividualCollections Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); else Penumbra.Messager.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", + $"Could not migrate {name} ({collection.Identity.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", NotificationType.Error); } // If it is not a valid NPC name, check if it can be a player name. @@ -192,16 +192,16 @@ public partial class IndividualCollections var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}.")); // Try to migrate the player name without logging full names. if (Add($"{name} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", [identifier], collection)) - Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); + Penumbra.Log.Information($"Migrated {shortName} ({collection.Identity.AnonymizedName}) to Player Identifier."); else Penumbra.Messager.NotificationMessage( - $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + $"Could not migrate {shortName} ({collection.Identity.AnonymizedName}), please look through your individual collections.", NotificationType.Error); } else { Penumbra.Messager.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", + $"Could not migrate {name} ({collection.Identity.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", NotificationType.Error); } } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index bc1a362c..e003ad6b 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -89,7 +89,7 @@ public class InheritanceManager : IDisposable, IService _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); - Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); + Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances."); } /// Order in the inheritance list is relevant. @@ -101,7 +101,7 @@ public class InheritanceManager : IDisposable, IService _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); - Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); + Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}."); } /// @@ -119,7 +119,7 @@ public class InheritanceManager : IDisposable, IService RecurseInheritanceChanges(inheritor); } - Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances."); + Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances."); return true; } @@ -143,23 +143,23 @@ public class InheritanceManager : IDisposable, IService continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) { changes = true; - Penumbra.Log.Information($"Migrating inheritance for {collection.AnonymizedName} from name to GUID."); + Penumbra.Log.Information($"Migrating inheritance for {collection.Identity.AnonymizedName} from name to GUID."); if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", + Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else { Penumbra.Messager.NotificationMessage( - $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", + $"Inherited collection {subCollectionName} for {collection.Identity.AnonymizedName} does not exist, it was removed.", NotificationType.Warning); changes = true; } diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index e5b844c8..8aab5297 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -44,7 +44,7 @@ public class TempCollectionManager : IDisposable, IService => _customCollections.Values; public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _customCollections.Values.FindFirst(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase), out collection); + => _customCollections.Values.FindFirst(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase), out collection); public bool CollectionById(Guid id, [NotNullWhen(true)] out ModCollection? collection) => _customCollections.TryGetValue(id, out collection); @@ -54,12 +54,12 @@ public class TempCollectionManager : IDisposable, IService if (GlobalChangeCounter == int.MaxValue) GlobalChangeCounter = 0; var collection = _storage.CreateTemporary(name, ~Count, GlobalChangeCounter++); - Penumbra.Log.Debug($"Creating temporary collection {collection.Name} with {collection.Id}."); - if (_customCollections.TryAdd(collection.Id, collection)) + Penumbra.Log.Debug($"Creating temporary collection {collection.Identity.Name} with {collection.Identity.Id}."); + if (_customCollections.TryAdd(collection.Identity.Id, collection)) { // Temporary collection created. _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, string.Empty); - return collection.Id; + return collection.Identity.Id; } return Guid.Empty; @@ -74,7 +74,7 @@ public class TempCollectionManager : IDisposable, IService } _storage.Delete(collection); - Penumbra.Log.Debug($"Deleted temporary collection {collection.Id}."); + Penumbra.Log.Debug($"Deleted temporary collection {collection.Identity.Id}."); GlobalChangeCounter += Math.Max(collection.Counters.Change + 1 - GlobalChangeCounter, 0); for (var i = 0; i < Collections.Count; ++i) { @@ -83,7 +83,7 @@ public class TempCollectionManager : IDisposable, IService // Temporary collection assignment removed. _communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName); - Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Id} from {Collections[i].DisplayName}."); + Penumbra.Log.Verbose($"Unassigned temporary collection {collection.Identity.Id} from {Collections[i].DisplayName}."); Collections.Delete(i--); } @@ -96,7 +96,7 @@ public class TempCollectionManager : IDisposable, IService return false; // Temporary collection assignment added. - Penumbra.Log.Verbose($"Assigned temporary collection {collection.AnonymizedName} to {Collections.Last().DisplayName}."); + Penumbra.Log.Verbose($"Assigned temporary collection {collection.Identity.AnonymizedName} to {Collections.Last().DisplayName}."); _communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName); return true; } @@ -127,6 +127,6 @@ public class TempCollectionManager : IDisposable, IService return false; var identifier = _actors.CreatePlayer(byteString, worldId); - return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Id); + return Collections.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Identity.Id); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 95e78da0..9b33c1f4 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -13,42 +13,21 @@ namespace Penumbra.Collections; /// - Index is the collections index in the ModCollection.Manager /// - Settings has the same size as ModManager.Mods. /// - any change in settings or inheritance of the collection causes a Save. -/// - the name can not contain invalid path characters and has to be unique when lower-cased. /// public partial class ModCollection { - public const int CurrentVersion = 2; - public const string DefaultCollectionName = "Default"; - public const string EmptyCollectionName = "None"; + public const int CurrentVersion = 2; /// /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(Guid.Empty, EmptyCollectionName, LocalCollectionId.Zero, 0, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, [], [], []); - /// The name of a collection. - public string Name { get; set; } - - public Guid Id { get; } - - public LocalCollectionId LocalId { get; } - - public string Identifier - => Id.ToString(); - - public string ShortIdentifier - => Identifier[..8]; + public ModCollectionIdentity Identity; public override string ToString() - => Name.Length > 0 ? Name : ShortIdentifier; - - /// Get the first two letters of a collection name and its Index (or None if it is the empty collection). - public string AnonymizedName - => this == Empty ? Empty.Name : Name == DefaultCollectionName ? Name : ShortIdentifier; - - /// The index of the collection is set and kept up-to-date by the CollectionManager. - public int Index { get; internal set; } + => Identity.ToString(); public CollectionCounters Counters; @@ -90,7 +69,7 @@ public partial class ModCollection { get { - if (Index <= 0) + if (Identity.Index <= 0) return (ModSettings.Empty, this); foreach (var collection in GetFlattenedInheritance()) @@ -114,17 +93,17 @@ public partial class ModCollection public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, Settings.Select(s => s?.DeepCopy()).ToList(), - [.. DirectlyInheritsFrom], UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, + Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], + UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); } /// Constructor for reading from files. - public static ModCollection CreateFromData(SaveService saver, ModStorage mods, Guid id, string name, LocalCollectionId localId, int version, - int index, + public static ModCollection CreateFromData(SaveService saver, ModStorage mods, ModCollectionIdentity identity, int version, Dictionary allSettings, IReadOnlyList inheritances) { - Debug.Assert(index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(id, name, localId, index, 0, version, [], [], allSettings) + Debug.Assert(identity.Index > 0, "Collection read with non-positive index."); + var ret = new ModCollection(identity, 0, version, [], [], allSettings) { InheritanceByName = inheritances, }; @@ -137,7 +116,7 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(Guid.NewGuid(), name, localId, index, changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, [], [], []); return ret; } @@ -145,9 +124,8 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(Guid.NewGuid(), name, localId, index, 0, CurrentVersion, - Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], - []); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, + Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); } /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. @@ -195,16 +173,13 @@ public partial class ModCollection saver.ImmediateSave(new ModCollectionSave(mods, this)); } - private ModCollection(Guid id, string name, LocalCollectionId localId, int index, int changeCounter, int version, - List appliedSettings, List inheritsFrom, Dictionary settings) + private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, List appliedSettings, + List inheritsFrom, Dictionary settings) { - Name = name; - Id = id; - LocalId = localId; - Index = index; + Identity = identity; Counters = new CollectionCounters(changeCounter); - Settings = appliedSettings; - UnusedSettings = settings; + Settings = appliedSettings; + UnusedSettings = settings; DirectlyInheritsFrom = inheritsFrom; foreach (var c in DirectlyInheritsFrom) ((List)c.DirectParentOf).Add(this); diff --git a/Penumbra/Collections/ModCollectionIdentity.cs b/Penumbra/Collections/ModCollectionIdentity.cs new file mode 100644 index 00000000..c7f60005 --- /dev/null +++ b/Penumbra/Collections/ModCollectionIdentity.cs @@ -0,0 +1,42 @@ +using OtterGui; +using Penumbra.Collections.Manager; + +namespace Penumbra.Collections; + +public struct ModCollectionIdentity(Guid id, LocalCollectionId localId) +{ + public const string DefaultCollectionName = "Default"; + public const string EmptyCollectionName = "None"; + + public static readonly ModCollectionIdentity Empty = new(Guid.Empty, LocalCollectionId.Zero, EmptyCollectionName, 0); + + public string Name { get; set; } + public Guid Id { get; } = id; + public LocalCollectionId LocalId { get; } = localId; + + /// The index of the collection is set and kept up-to-date by the CollectionManager. + public int Index { get; internal set; } + + public string Identifier + => Id.ToString(); + + public string ShortIdentifier + => Id.ShortGuid(); + + /// Get the short identifier of a collection unless it is a well-known collection name. + public string AnonymizedName + => Id == Guid.Empty ? EmptyCollectionName : Name == DefaultCollectionName ? Name : ShortIdentifier; + + public override string ToString() + => Name.Length > 0 ? Name : ShortIdentifier; + + public ModCollectionIdentity(Guid id, LocalCollectionId localId, string name, int index) + : this(id, localId) + { + Name = name; + Index = index; + } + + public static ModCollectionIdentity New(string name, LocalCollectionId id, int index) + => new(Guid.NewGuid(), id, name, index); +} diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index e6bb069b..6e1b51ac 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -15,7 +15,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection => fileNames.CollectionFile(modCollection); public string LogName(string _) - => modCollection.AnonymizedName; + => modCollection.Identity.AnonymizedName; public string TypeName => "Collection"; @@ -28,10 +28,10 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteStartObject(); j.WritePropertyName("Version"); j.WriteValue(ModCollection.CurrentVersion); - j.WritePropertyName(nameof(ModCollection.Id)); - j.WriteValue(modCollection.Identifier); - j.WritePropertyName(nameof(ModCollection.Name)); - j.WriteValue(modCollection.Name); + j.WritePropertyName(nameof(ModCollectionIdentity.Id)); + j.WriteValue(modCollection.Identity.Identifier); + j.WritePropertyName(nameof(ModCollectionIdentity.Name)); + j.WriteValue(modCollection.Identity.Name); j.WritePropertyName(nameof(ModCollection.Settings)); // Write all used and unused settings by mod directory name. @@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identifier)); + x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identity.Identifier)); j.WriteEndObject(); } @@ -79,8 +79,8 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection { var obj = JObject.Parse(File.ReadAllText(file.FullName)); version = obj["Version"]?.ToObject() ?? 0; - name = obj[nameof(ModCollection.Name)]?.ToObject() ?? string.Empty; - id = obj[nameof(ModCollection.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); + name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject() ?? string.Empty; + id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index 8fe160b3..bda877ff 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -23,7 +23,7 @@ public readonly struct ResolveData(ModCollection collection, nint gameObject) { } public override string ToString() - => ModCollection.Name; + => ModCollection.Identity.Name; } public static class ResolveDataExtensions diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index aff7f16f..61946978 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -326,7 +326,7 @@ public class CommandHandler : IDisposable, IApiService { _chat.Print(collection == null ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" - : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); + : $"{collection.Identity.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); continue; } @@ -363,13 +363,13 @@ public class CommandHandler : IDisposable, IApiService } Print( - $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); + $"Removed {oldCollection.Identity.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); anySuccess = true; continue; } _collectionManager.Active.SetCollection(collection!, type, individualIndex); - Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); + Print($"Assigned {collection!.Identity.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); } return anySuccess; @@ -440,7 +440,7 @@ public class CommandHandler : IDisposable, IApiService _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) .AddText("already had the desired state in collection ") - .AddYellow(collection!.Name, true).AddText(".").BuiltString); + .AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString); return false; } @@ -458,7 +458,7 @@ public class CommandHandler : IDisposable, IApiService _collectionEditor.SetModSetting(collection!, mod, groupIndex, setting); Print(() => new SeStringBuilder().AddText("Changed settings of group ").AddGreen(groupName, true).AddText(" in mod ") .AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection!.Name, true).AddText(".").BuiltString); + .AddYellow(collection!.Identity.Name, true).AddText(".").BuiltString); return true; } @@ -543,7 +543,7 @@ public class CommandHandler : IDisposable, IApiService changes |= HandleModState(state, collection!, mod); if (!changes) - Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true) + Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -558,7 +558,7 @@ public class CommandHandler : IDisposable, IApiService return true; } - collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) + collection = string.Equals(lowerName, ModCollection.Empty.Identity.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty : _collectionManager.Storage.ByIdentifier(lowerName, out var c) ? c @@ -614,7 +614,7 @@ public class CommandHandler : IDisposable, IApiService return false; Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -623,7 +623,7 @@ public class CommandHandler : IDisposable, IApiService return false; Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -634,7 +634,7 @@ public class CommandHandler : IDisposable, IApiService Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) .AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(".").BuiltString); return true; @@ -643,7 +643,7 @@ public class CommandHandler : IDisposable, IApiService return false; Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") - .AddYellow(collection.Name, true) + .AddYellow(collection.Identity.Name, true) .AddText(" to inherit.").BuiltString); return true; } diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index dcbaedc8..c350c157 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -30,7 +30,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); Penumbra.Log.Excessive( - $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.AnonymizedName}."); + $"[AtchCaller1] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, identified to {collection.ModCollection.Identity.AnonymizedName}."); } public void Dispose() diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs index aa2d3f31..af38ce50 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook2.cs @@ -30,7 +30,7 @@ public unsafe class AtchCallerHook2 : FastHook, IDispo Task.Result.Original(data, slot, unk, playerModel, unk2); _metaState.AtchCollection.Pop(); Penumbra.Log.Excessive( - $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.AnonymizedName}."); + $"[AtchCaller2] Invoked on 0x{(ulong)data:X} with {slot}, {unk:X}, 0x{playerModel.Address:X}, {unk2}, identified to {collection.ModCollection.Identity.AnonymizedName}."); } public void Dispose() diff --git a/Penumbra/Interop/PathResolving/MetaState.cs b/Penumbra/Interop/PathResolving/MetaState.cs index e7fc3176..eeae77cc 100644 --- a/Penumbra/Interop/PathResolving/MetaState.cs +++ b/Penumbra/Interop/PathResolving/MetaState.cs @@ -98,7 +98,7 @@ public sealed unsafe class MetaState : IDisposable, IService _lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true); if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero) _communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject, - _lastCreatedCollection.ModCollection.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); + _lastCreatedCollection.ModCollection.Identity.Id, (nint)modelCharaId, (nint)customize, (nint)equipData); var decal = new DecalReverter(Config, _characterUtility, _resources, _lastCreatedCollection, UsesDecal(*(uint*)modelCharaId, (nint)customize)); diff --git a/Penumbra/Interop/PathResolving/PathDataHandler.cs b/Penumbra/Interop/PathResolving/PathDataHandler.cs index 25d4f7ea..e0c235a2 100644 --- a/Penumbra/Interop/PathResolving/PathDataHandler.cs +++ b/Penumbra/Interop/PathResolving/PathDataHandler.cs @@ -32,7 +32,7 @@ public static class PathDataHandler /// Create the encoding path for an IMC file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateImc(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.Counters.Imc}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Imc}_{DiscriminatorString}|{path}"); /// Create the encoding path for a TMB file. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -47,17 +47,17 @@ public static class PathDataHandler /// Create the encoding path for an ATCH file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateAtch(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.Counters.Atch}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Atch}_{DiscriminatorString}|{path}"); /// Create the encoding path for a MTRL file. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static FullPath CreateMtrl(CiByteString path, ModCollection collection, Utf8GamePath originalPath) - => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Change}_{originalPath.Path.Crc32:X8}_{DiscriminatorString}|{path}"); /// The base function shared by most file types. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static FullPath CreateBase(CiByteString path, ModCollection collection) - => new($"|{collection.LocalId.Id}_{collection.Counters.Change}_{DiscriminatorString}|{path}"); + => new($"|{collection.Identity.LocalId.Id}_{collection.Counters.Change}_{DiscriminatorString}|{path}"); /// Read an additional data blurb and parse it into usable data for all file types but Materials. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index a3233cfb..513877d4 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -26,6 +26,6 @@ public sealed class ImcFilePostProcessor(CollectionStorage collections) : IFileP file.Replace(resource); Penumbra.Log.Verbose( - $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.AnonymizedName}."); + $"[ResourceLoader] Loaded {originalGamePath} from file and replaced with IMC from collection {collection.Identity.AnonymizedName}."); } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 7e378f41..f5659e7c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -81,7 +81,7 @@ public class ResourceTreeFactory( var (name, anonymizedName, related) = GetCharacterName(character); var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, - networked, collectionResolveData.ModCollection.Name, collectionResolveData.ModCollection.AnonymizedName); + networked, collectionResolveData.ModCollection.Identity.Name, collectionResolveData.ModCollection.Identity.AnonymizedName); var globalContext = new GlobalResolveContext(metaFileManager, objectIdentifier, collectionResolveData.ModCollection, cache, (flags & Flags.WithUiData) != 0); using (var _ = pathState.EnterInternalResolve()) diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index b5499624..8fdd09c5 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -63,10 +63,10 @@ public class TemporaryMod : IMod DirectoryInfo? dir = null; try { - dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Name, config.ReplaceNonAsciiOnImport, true); + dir = ModCreator.CreateModFolder(modManager.BasePath, collection.Identity.Name, config.ReplaceNonAsciiOnImport, true); var fileDir = Directory.CreateDirectory(Path.Combine(dir.FullName, "files")); - modManager.DataEditor.CreateMeta(dir, collection.Name, character ?? config.DefaultModAuthor, - $"Mod generated from temporary collection {collection.Id} for {character ?? "Unknown Character"} with name {collection.Name}.", + modManager.DataEditor.CreateMeta(dir, collection.Identity.Name, character ?? config.DefaultModAuthor, + $"Mod generated from temporary collection {collection.Identity.Id} for {character ?? "Unknown Character"} with name {collection.Identity.Name}.", null, null); var mod = new Mod(dir); var defaultMod = mod.Default; @@ -99,11 +99,11 @@ public class TemporaryMod : IMod saveService.ImmediateSaveSync(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir, false); Penumbra.Log.Information( - $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identifier}."); + $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Identity.Identifier}."); } catch (Exception e) { - Penumbra.Log.Error($"Could not save temporary collection {collection.Identifier} to permanent Mod:\n{e}"); + Penumbra.Log.Error($"Could not save temporary collection {collection.Identity.Identifier} to permanent Mod:\n{e}"); if (dir != null && Directory.Exists(dir.FullName)) { try diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 534911df..a2594145 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -245,24 +245,24 @@ public class Penumbra : IDalamudPlugin void PrintCollection(ModCollection c, CollectionCache _) => sb.Append( - $"> **`Collection {c.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); + $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n"); sb.Append($"> **`Active Collections: `** {_collectionManager.Caches.Count}\n"); - sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.AnonymizedName}\n"); + sb.Append($"> **`Base Collection: `** {_collectionManager.Active.Default.Identity.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {_collectionManager.Active.Interface.Identity.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {_collectionManager.Active.Current.Identity.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { var collection = _collectionManager.Active.ByType(type); if (collection != null) - sb.Append($"> **`{name,-29}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{name,-29}`** {collection.Identity.AnonymizedName}\n"); } foreach (var (name, id, collection) in _collectionManager.Active.Individuals.Assignments) - sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.AnonymizedName}\n"); + sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.Identity.AnonymizedName}\n"); foreach (var collection in _collectionManager.Caches.Active) PrintCollection(collection, collection._cache!); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 5ba57cf4..f58eb891 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -27,8 +27,8 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu private Configuration _config = null!; private JObject _data = null!; - public string CurrentCollection = ModCollection.DefaultCollectionName; - public string DefaultCollection = ModCollection.DefaultCollectionName; + public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName; + public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = []; public Dictionary ModSortOrder = []; @@ -346,7 +346,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu if (!collectionJson.Exists) return; - var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); + var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName)); if (defaultCollectionFile.Exists) return; @@ -380,7 +380,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu var emptyStorage = new ModStorage(); // Only used for saving and immediately discarded, so the local collection id here is irrelevant. - var collection = ModCollection.CreateFromData(saveService, emptyStorage, Guid.NewGuid(), ModCollection.DefaultCollectionName, LocalCollectionId.Zero, 0, 1, dict, []); + var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) diff --git a/Penumbra/Services/CrashHandlerService.cs b/Penumbra/Services/CrashHandlerService.cs index 9103b29c..4814795c 100644 --- a/Penumbra/Services/CrashHandlerService.cs +++ b/Penumbra/Services/CrashHandlerService.cs @@ -240,7 +240,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(character); lock (_eventWriter) { - _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Id, type); + _eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Identity.Id, type); } } catch (Exception ex) @@ -293,7 +293,7 @@ public sealed class CrashHandlerService : IDisposable, IService var name = GetActorName(resolveData.AssociatedGameObject); lock (_eventWriter) { - _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Id, + _eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Identity.Id, manipulatedPath.Value.InternalName.Span, originalPath.Path.Span); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 817af0d2..ee096109 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -24,7 +24,7 @@ public class FilenameService(IDalamudPluginInterface pi) : IService /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) - => CollectionFile(collection.Identifier); + => CollectionFile(collection.Identity.Identifier); /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 3972e350..0e1408c5 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -25,7 +25,7 @@ public class CollectionSelectHeader : IUiService _selection = selection; _resolver = resolver; _activeCollections = collectionManager.Active; - _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Name).ToList()); + _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Identity.Name).ToList()); } /// Draw the header line that can quick switch between collections. @@ -77,10 +77,10 @@ public class CollectionSelectHeader : IUiService return CheckCollection(collection) switch { CollectionState.Empty => (collection, "None", "The base collection is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Name, + CollectionState.Selected => (collection, collection.Identity.Name, "The configured base collection is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Name, - $"Select the configured base collection {collection.Name} as the current collection.", false), + CollectionState.Available => (collection, collection.Identity.Name, + $"Select the configured base collection {collection.Identity.Name} as the current collection.", false), _ => throw new Exception("Can not happen."), }; } @@ -91,10 +91,11 @@ public class CollectionSelectHeader : IUiService return CheckCollection(collection) switch { CollectionState.Empty => (collection, "None", "The loaded player character is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Name, + CollectionState.Selected => (collection, collection.Identity.Name, "The collection configured to apply to the loaded player character is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Name, - $"Select the collection {collection.Name} that applies to the loaded player character as the current collection.", false), + CollectionState.Available => (collection, collection.Identity.Name, + $"Select the collection {collection.Identity.Name} that applies to the loaded player character as the current collection.", + false), _ => throw new Exception("Can not happen."), }; } @@ -105,10 +106,10 @@ public class CollectionSelectHeader : IUiService return CheckCollection(collection) switch { CollectionState.Empty => (collection, "None", "The interface collection is configured to use no mods.", true), - CollectionState.Selected => (collection, collection.Name, + CollectionState.Selected => (collection, collection.Identity.Name, "The configured interface collection is already selected as the current collection.", true), - CollectionState.Available => (collection, collection.Name, - $"Select the configured interface collection {collection.Name} as the current collection.", false), + CollectionState.Available => (collection, collection.Identity.Name, + $"Select the configured interface collection {collection.Identity.Name} as the current collection.", false), _ => throw new Exception("Can not happen."), }; } @@ -120,8 +121,8 @@ public class CollectionSelectHeader : IUiService { CollectionState.Unavailable => (null, "Not Inherited", "The settings of the selected mod are not inherited from another collection.", true), - CollectionState.Available => (collection, collection!.Name, - $"Select the collection {collection!.Name} from which the selected mod inherits its settings as the current collection.", + CollectionState.Available => (collection, collection!.Identity.Name, + $"Select the collection {collection!.Identity.Name} from which the selected mod inherits its settings as the current collection.", false), _ => throw new Exception("Can not happen."), }; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 1670be5e..0259713f 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -29,13 +29,13 @@ public sealed class CollectionCombo(CollectionManager manager, Func obj.Name; + => obj.Identity.Name; protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, ImGuiComboFlags flags) diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 914f10d9..cab34b10 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -221,16 +221,16 @@ public sealed class CollectionPanel( ImGui.SameLine(); ImGui.BeginGroup(); using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); - var name = _newName ?? collection.Name; - var identifier = collection.Identifier; + 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.Name) + if (ImGui.IsItemDeactivatedAfterEdit() && _newName != null && _newName != collection.Identity.Name) { - collection.Name = _newName; + collection.Identity.Name = _newName; saveService.QueueSave(new ModCollectionSave(mods, collection)); selector.RestoreCollections(); _newName = null; @@ -242,7 +242,7 @@ public sealed class CollectionPanel( using (ImRaii.PushFont(UiBuilder.MonoFont)) { - if (ImGui.Button(collection.Identifier, new Vector2(width, 0))) + if (ImGui.Button(collection.Identity.Identifier, new Vector2(width, 0))) try { Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); @@ -289,9 +289,9 @@ public sealed class CollectionPanel( _active.SetCollection(null, type, _active.Individuals.GetGroup(identifier)); } - foreach (var coll in _collections.OrderBy(c => c.Name)) + foreach (var coll in _collections.OrderBy(c => c.Identity.Name)) { - if (coll != collection && ImGui.MenuItem($"Use {coll.Name}.")) + if (coll != collection && ImGui.MenuItem($"Use {coll.Identity.Name}.")) _active.SetCollection(coll, type, _active.Individuals.GetGroup(identifier)); } } @@ -418,7 +418,7 @@ public sealed class CollectionPanel( private string Name(ModCollection? collection) => collection == null ? "Unassigned" : collection == ModCollection.Empty ? "Use No Mods" : - incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; + incognito.IncognitoMode ? collection.Identity.AnonymizedName : collection.Identity.Name; private void DrawIndividualButton(string intro, Vector2 width, string tooltip, char suffix, params ActorIdentifier[] identifiers) { diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 024873bf..57429531 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -69,7 +69,7 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } protected override bool Filtered(int idx) - => !Items[idx].Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); + => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); private const string PayloadString = "Collection"; @@ -111,12 +111,12 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl } private string Name(ModCollection collection) - => _incognito.IncognitoMode || collection.Name.Length == 0 ? collection.AnonymizedName : collection.Name; + => _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name; public void RestoreCollections() { Items.Clear(); - foreach (var c in _storage.OrderBy(c => c.Name)) + foreach (var c in _storage.OrderBy(c => c.Identity.Name)) Items.Add(c); SetFilterDirty(); SetCurrent(_active.Current); diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 418fe52c..a4d60b13 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -93,7 +93,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService /// private void DrawInheritedChildren(ModCollection collection) { - using var id = ImRaii.PushId(collection.Index); + using var id = ImRaii.PushId(collection.Identity.Index); using var indent = ImRaii.PushIndent(); // Get start point for the lines (top of the selector). @@ -114,7 +114,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService _seenInheritedCollections.Contains(inheritance)); _seenInheritedCollections.Add(inheritance); - ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Id}", + ImRaii.TreeNode($"{Name(inheritance)}###{inheritance.Identity.Id}", ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); DrawInheritanceTreeClicks(inheritance, false); @@ -140,7 +140,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), _seenInheritedCollections.Contains(collection)); _seenInheritedCollections.Add(collection); - using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen); + using var tree = ImRaii.TreeNode($"{Name(collection)}###{collection.Identity.Name}", ImGuiTreeNodeFlags.NoTreePushOnOpen); color.Pop(); DrawInheritanceTreeClicks(collection, true); DrawInheritanceDropSource(collection); @@ -252,7 +252,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService foreach (var collection in _collections .Where(c => InheritanceManager.CheckValidInheritance(_active.Current, c) == InheritanceManager.ValidInheritance.Valid) - .OrderBy(c => c.Name)) + .OrderBy(c => c.Identity.Name)) { if (ImGui.Selectable(Name(collection), _newInheritance == collection)) _newInheritance = collection; @@ -312,5 +312,5 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService } private string Name(ModCollection collection) - => incognito.IncognitoMode ? collection.AnonymizedName : collection.Name; + => incognito.IncognitoMode ? collection.Identity.AnonymizedName : collection.Identity.Name; } diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index b7648428..89a7d765 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -56,7 +56,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele foreach (var ((collection, parent, color, state), idx) in _cache.WithIndex()) { using var id = ImUtf8.PushId(idx); - ImUtf8.DrawTableColumn(collection.Name); + ImUtf8.DrawTableColumn(collection.Identity.Name); ImGui.TableNextColumn(); ImUtf8.Text(ToText(state), color); @@ -65,7 +65,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele { if (context) { - ImUtf8.Text(collection.Name); + ImUtf8.Text(collection.Identity.Name); ImGui.Separator(); using (ImRaii.Disabled(state is ModState.Enabled && parent == collection)) { @@ -95,7 +95,7 @@ public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSele } } - ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Name); + ImUtf8.DrawTableColumn(parent == collection ? string.Empty : parent.Identity.Name); } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 8d889c3b..261f6e92 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -67,7 +67,7 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {selection.Collection.Name}.", width)) + if (ImGui.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width)) collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index d432e97e..0f72efff 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -241,7 +241,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService { var pathString = manipulatedPath != null ? $"custom file {name2} instead of {name}" : name; Penumbra.Log.Information( - $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); + $"[ResourceLoader] [LOAD] [{handle->FileType}] Loaded {pathString} to 0x{(ulong)handle:X} using collection {data.ModCollection.Identity.AnonymizedName} for {Name(data, "no associated object.")} (Refcount {handle->RefCount}) "); } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 88b7120d..7ac3cb99 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -167,7 +167,7 @@ internal sealed class ResourceWatcherTable : Table => 80 * UiHelpers.Scale; public override string ToName(Record item) - => item.Collection?.Name ?? string.Empty; + => (item.Collection != null ? item.Collection.Identity.Name : null) ?? string.Empty; } private sealed class ObjectColumn : ColumnString diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 95afb10f..d6a9f05a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -204,7 +204,7 @@ public class DebugTab : Window, ITab, IUiService if (collection.HasCache) { using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value()); - using var node = TreeNode($"{collection.Name} (Change Counter {collection.Counters.Change})###{collection.Name}"); + using var node = TreeNode($"{collection.Identity.Name} (Change Counter {collection.Counters.Change})###{collection.Identity.Name}"); if (!node) continue; @@ -239,7 +239,7 @@ public class DebugTab : Window, ITab, IUiService else { using var color = PushColor(ImGuiCol.Text, ColorId.UndefinedMod.Value()); - TreeNode($"{collection.AnonymizedName} (Change Counter {collection.Counters.Change})", + TreeNode($"{collection.Identity.AnonymizedName} (Change Counter {collection.Counters.Change})", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); } } @@ -265,9 +265,9 @@ public class DebugTab : Window, ITab, IUiService { PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); PrintValue("Git Commit Hash", _validityChecker.CommitHash); - PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Identity.Name); PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); - PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Identity.Name); PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); @@ -518,7 +518,7 @@ public class DebugTab : Window, ITab, IUiService return; ImGui.TextUnformatted( - $"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Name})"); + $"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Identity.Name})"); using (var drawTree = TreeNode("Draw Object to Object")) { if (drawTree) @@ -545,7 +545,7 @@ public class DebugTab : Window, ITab, IUiService ImGui.TextUnformatted(name); ImGui.TableNextColumn(); var collection = _collectionResolver.IdentifyCollection(gameObject, true); - ImGui.TextUnformatted(collection.ModCollection.Name); + ImGui.TextUnformatted(collection.ModCollection.Identity.Name); } } } @@ -561,7 +561,7 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"{data.AssociatedGameObject:X}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted(data.ModCollection.Name); + ImGui.TextUnformatted(data.ModCollection.Identity.Name); } } } @@ -574,12 +574,12 @@ public class DebugTab : Window, ITab, IUiService if (table) { ImGuiUtil.DrawTableColumn("Current Mtrl Data"); - ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Name); + ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Identity.Name); ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.MtrlData.AssociatedGameObject:X}"); ImGui.TableNextColumn(); ImGuiUtil.DrawTableColumn("Current Avfx Data"); - ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Name); + ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Identity.Name); ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.AvfxData.AssociatedGameObject:X}"); ImGui.TableNextColumn(); @@ -591,7 +591,7 @@ public class DebugTab : Window, ITab, IUiService foreach (var (resource, resolve) in _subfileHelper) { ImGuiUtil.DrawTableColumn($"0x{resource:X}"); - ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name); + ImGuiUtil.DrawTableColumn(resolve.ModCollection.Identity.Name); ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}"); ImGuiUtil.DrawTableColumn($"{((ResourceHandle*)resource)->FileName()}"); } @@ -611,7 +611,7 @@ public class DebugTab : Window, ITab, IUiService ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}"); ImGuiUtil.DrawTableColumn($"0x{address:X}"); ImGuiUtil.DrawTableColumn(identifier.ToString()); - ImGuiUtil.DrawTableColumn(collection.Name); + ImGuiUtil.DrawTableColumn(collection.Identity.Name); } } } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 87338bdb..c226098d 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -77,12 +77,12 @@ public class ModsTab( { Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); Penumbra.Log.Error($"{modManager.Count} Mods\n" - + $"{_activeCollections.Current.AnonymizedName} Current Collection\n" + + $"{_activeCollections.Current.Identity.AnonymizedName} Current Collection\n" + $"{_activeCollections.Current.Settings.Count} Settings\n" + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"); + + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); } } diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index 7d2a0d2a..69f2b616 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -83,14 +83,14 @@ public class TutorialService : IUiService + "Go here after setting up your root folder to continue the tutorial!") .Register("Initial Setup, Step 4: Managing Collections", "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n" - + $"There will always be one collection called {ModCollection.DefaultCollectionName} that can not be deleted.") + + $"There will always be one collection called {ModCollectionIdentity.DefaultCollectionName} that can not be deleted.") .Register($"Initial Setup, Step 5: {SelectedCollection}", $"The {SelectedCollection} is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n" - + $"We should already have the collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") + + $"We should already have the collection named {ModCollectionIdentity.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") .Register("Initial Setup, Step 6: Simple Assignments", "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n" + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n" - + $"If you are just starting, you can see that the {ModCollection.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" + + $"If you are just starting, you can see that the {ModCollectionIdentity.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons.") .Register("Individual Assignments", "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.") From 98a89bb2b4b67a2767773ec0f18c9b346028dc07 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 16:02:50 +0100 Subject: [PATCH 505/865] Current state. --- Penumbra/Api/Api/ModSettingsApi.cs | 154 ++++++++++++++- Penumbra/Collections/Cache/CollectionCache.cs | 6 +- .../Cache/CollectionCacheManager.cs | 16 +- .../Collections/Manager/ActiveCollections.cs | 2 +- .../Collections/Manager/CollectionEditor.cs | 40 ++-- .../Collections/Manager/CollectionStorage.cs | 28 +-- .../Collections/Manager/InheritanceManager.cs | 46 ++--- .../Manager/ModCollectionMigration.cs | 8 +- Penumbra/Collections/ModCollection.cs | 183 +++++++----------- .../Collections/ModCollectionInheritance.cs | 92 +++++++++ Penumbra/Collections/ModCollectionSave.cs | 12 +- Penumbra/Collections/ModSettingProvider.cs | 98 ++++++++++ Penumbra/CommandHandler.cs | 2 +- Penumbra/Interop/ResourceTree/ResourceNode.cs | 2 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 3 + Penumbra/Mods/ModSelection.cs | 21 +- Penumbra/Mods/Settings/ModSettings.cs | 2 +- Penumbra/Penumbra.cs | 2 +- Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 4 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 21 +- Penumbra/UI/CollectionTab/InheritanceUi.cs | 16 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 28 ++- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 37 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 40 +++- Penumbra/UI/Tabs/ModsTab.cs | 2 +- 28 files changed, 606 insertions(+), 265 deletions(-) create mode 100644 Penumbra/Collections/ModCollectionInheritance.cs create mode 100644 Penumbra/Collections/ModSettingProvider.cs diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 3dc900fc..4027975b 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -24,18 +25,20 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private readonly CollectionManager _collectionManager; private readonly CollectionEditor _collectionEditor; private readonly CommunicatorService _communicator; + private readonly ApiHelpers _helpers; public ModSettingsApi(CollectionResolver collectionResolver, ModManager modManager, CollectionManager collectionManager, CollectionEditor collectionEditor, - CommunicatorService communicator) + CommunicatorService communicator, ApiHelpers helpers) { _collectionResolver = collectionResolver; _modManager = modManager; _collectionManager = collectionManager; _collectionEditor = collectionEditor; _communicator = communicator; + _helpers = helpers; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); @@ -63,11 +66,6 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return new AvailableModSettings(dict); } - public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) - => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)) - : null; - public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance) { @@ -80,14 +78,14 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var settings = collection.Identity.Id == Guid.Empty ? null : ignoreInheritance - ? collection.Settings[mod.Index] - : collection[mod.Index].Settings; + ? collection.GetOwnSettings(mod.Index) + : collection.GetInheritedSettings(mod.Index).Settings; if (settings == null) return (PenumbraApiEc.Success, null); var (enabled, priority, dict) = settings.ConvertToShareable(mod); return (PenumbraApiEc.Success, - (enabled, priority.Value, dict, collection.Settings[mod.Index] == null)); + (enabled, priority.Value, dict, collection.GetOwnSettings(mod.Index) is null)); } public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) @@ -211,11 +209,147 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } + public PenumbraApiEc SetTemporaryModSetting(Guid collectionId, string modDirectory, string modName, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return SetTemporaryModSetting(args, collection, modDirectory, modName, enabled, priority, options, source, key); + } + + public PenumbraApiEc TemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + return PenumbraApiEc.Success; + } + + private PenumbraApiEc SetTemporaryModSetting(in LazyString args, ModCollection collection, string modDirectory, string modName, + bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is { } settings && settings.Lock != 0 && settings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + settings = new TemporaryModSettings + { + Enabled = enabled, + Priority = new ModPriority(priority), + Lock = key, + Source = source, + Settings = SettingList.Default(mod), + }; + + foreach (var (groupName, optionNames) in options) + { + var groupIdx = mod.Groups.IndexOf(g => g.Name == groupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var setting = Setting.Zero; + switch (mod.Groups[groupIdx]) + { + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting = Setting.Single(optionIdx); + break; + } + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.Options.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + settings.Settings[groupIdx] = setting; + } + + collection.Settings.SetTemporary(mod.Index, settings); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is not { } settings) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (settings.Lock != 0 && settings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + collection.Settings.SetTemporary(mod.Index, null); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) + { + return PenumbraApiEc.Success; + } + + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) + { + return PenumbraApiEc.Success; + } + + private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) + { + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is { } settings && (settings.Lock == 0 || settings.Lock == key)) + { + collection.Settings.SetTemporary(i, null); + ++numRemoved; + } + } + + return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { var collection = _collectionResolver.PlayerCollection(); - var (settings, parent) = collection[mod.Index]; + var (settings, parent) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection); } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index ad902aac..8ca9aa36 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -260,7 +260,7 @@ public sealed class CollectionCache : IDisposable if (mod.Index < 0) return mod.GetData(); - var settings = _collection[mod.Index].Settings; + var settings = _collection.GetActualSettings(mod.Index).Settings; return settings is not { Enabled: true } ? AppliedModData.Empty : mod.GetData(settings); @@ -342,8 +342,8 @@ public sealed class CollectionCache : IDisposable // Returns if the added mod takes priority before the existing mod. private bool AddConflict(object data, IMod addedMod, IMod existingMod) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority; + var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; if (existingPriority < addedPriority) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 0a851154..839c0376 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -231,11 +231,11 @@ public class CollectionCacheManager : IDisposable, IService { case ModPathChangeType.Deleted: case ModPathChangeType.StartingReload: - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.RemoveMod(mod, true); break; case ModPathChangeType.Moved: - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); break; } @@ -246,7 +246,7 @@ public class CollectionCacheManager : IDisposable, IService if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded)) return; - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.AddMod(mod, true); } @@ -273,7 +273,7 @@ public class CollectionCacheManager : IDisposable, IService { if (type is ModOptionChangeType.PrepareChange) { - foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true })) collection._cache!.RemoveMod(mod, false); return; @@ -284,7 +284,7 @@ public class CollectionCacheManager : IDisposable, IService if (!recomputeList) return; - foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true })) { if (justAdd) collection._cache!.AddMod(mod, true); @@ -317,7 +317,7 @@ public class CollectionCacheManager : IDisposable, IService cache.AddMod(mod!, true); else if (oldValue == Setting.True) cache.RemoveMod(mod!, true); - else if (collection[mod!.Index].Settings?.Enabled == true) + else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true) cache.ReloadMod(mod!, true); else cache.RemoveMod(mod!, true); @@ -329,8 +329,8 @@ public class CollectionCacheManager : IDisposable, IService break; case ModSettingChange.Setting: - if (collection[mod!.Index].Settings?.Enabled == true) - cache.ReloadMod(mod!, true); + if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true) + cache.ReloadMod(mod, true); break; case ModSettingChange.MultiInheritance: diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 07fcb430..2ced8ad6 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -282,7 +282,7 @@ public class ActiveCollections : ISavable, IDisposable, IService .Prepend(Interface) .Prepend(Default) .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + .SelectMany(c => c.Inheritance.FlatHierarchy).Contains(Current); /// Save if any of the active collections is changed and set new collections to Current. private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index caff2c86..66578a95 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -26,12 +26,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModState(ModCollection collection, Mod mod, bool newValue) { - var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false; + var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Enabled ?? false; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Enabled = newValue; + collection.GetOwnSettings(mod.Index)!.Enabled = newValue; InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True, 0); return true; @@ -55,13 +55,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu var changes = false; foreach (var mod in mods) { - var oldValue = collection.Settings[mod.Index]?.Enabled; + var oldValue = collection.GetOwnSettings(mod.Index)?.Enabled; if (newValue == oldValue) continue; FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Enabled = newValue; - changes = true; + collection.GetOwnSettings(mod.Index)!.Enabled = newValue; + changes = true; } if (!changes) @@ -76,12 +76,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue) { - var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default; + var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Priority ?? ModPriority.Default; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Priority = newValue; + collection.GetOwnSettings(mod.Index)!.Priority = newValue; InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0); return true; } @@ -92,15 +92,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { - var settings = collection.Settings[mod.Index] != null - ? collection.Settings[mod.Index]!.Settings - : collection[mod.Index].Settings?.Settings; + var settings = collection.GetInheritedSettings(mod.Index).Settings?.Settings; var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings; if (oldValue == newValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue); + collection.GetOwnSettings(mod.Index)!.SetValue(mod, groupIdx, newValue); InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx); return true; } @@ -115,10 +113,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu // If it does not exist, check unused settings. // If it does not exist and has no unused settings, also use null. ModSettings.SavedSettings? savedSettings = sourceMod != null - ? collection.Settings[sourceMod.Index] != null - ? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod) + ? collection.GetOwnSettings(sourceMod.Index) is { } ownSettings + ? new ModSettings.SavedSettings(ownSettings, sourceMod) : null - : collection.UnusedSettings.TryGetValue(sourceName, out var s) + : collection.Settings.Unused.TryGetValue(sourceName, out var s) ? s : null; @@ -148,10 +146,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu // or remove any unused settings for the target if they are inheriting. if (savedSettings != null) { - ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; + ((Dictionary)collection.Settings.Unused)[targetName] = savedSettings.Value; saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } - else if (((Dictionary)collection.UnusedSettings).Remove(targetName)) + else if (((Dictionary)collection.Settings.Unused).Remove(targetName)) { saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } @@ -166,12 +164,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit) { - var settings = collection.Settings[mod.Index]; + var settings = collection.GetOwnSettings(mod.Index); if (inherit == (settings == null)) return false; - ((List)collection.Settings)[mod.Index] = - inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + ModSettings? settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + collection.Settings.Set(mod.Index, settings1); return true; } @@ -188,7 +186,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - foreach (var directInheritor in directParent.DirectParentOf) + foreach (var directInheritor in directParent.Inheritance.DirectlyInheritedBy) { switch (type) { @@ -197,7 +195,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); break; default: - if (directInheritor.Settings[mod!.Index] == null) + if (directInheritor.GetOwnSettings(mod!.Index) == null) communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); break; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 2ed395ae..e19acd35 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -41,8 +41,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, IReadOnlyList inheritances) { - var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, - inheritances); + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, + new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); _collectionsByLocal[CurrentCollectionId] = newCollection; CurrentCollectionId += 1; return newCollection; @@ -196,8 +196,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// Remove all settings for not currently-installed mods from the given collection. public void CleanUnavailableSettings(ModCollection collection) { - var any = collection.UnusedSettings.Count > 0; - ((Dictionary)collection.UnusedSettings).Clear(); + var any = collection.Settings.Unused.Count > 0; + ((Dictionary)collection.Settings.Unused).Clear(); if (any) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } @@ -205,7 +205,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// Remove a specific setting for not currently-installed mods from the given collection. public void CleanUnavailableSetting(ModCollection collection, string? setting) { - if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting)) + if (setting != null && ((Dictionary)collection.Settings.Unused).Remove(setting)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } @@ -307,7 +307,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer private void OnModDiscoveryStarted() { foreach (var collection in this) - collection.PrepareModDiscovery(_modStorage); + collection.Settings.PrepareModDiscovery(_modStorage); } /// Restore all settings in all collections to mods. @@ -315,7 +315,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { // Re-apply all mod settings. foreach (var collection in this) - collection.ApplyModSettings(_saveService, _modStorage); + collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); } /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. @@ -326,21 +326,22 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { case ModPathChangeType.Added: foreach (var collection in this) - collection.AddMod(mod); + collection.Settings.AddMod(mod); break; case ModPathChangeType.Deleted: foreach (var collection in this) - collection.RemoveMod(mod); + collection.Settings.RemoveMod(mod); break; case ModPathChangeType.Moved: - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; case ModPathChangeType.Reloaded: foreach (var collection in this) { - if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false) + if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(mod.Index, null); } break; @@ -357,8 +358,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer foreach (var collection in this) { - if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) + if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(mod.Index, null); } } @@ -370,7 +372,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer foreach (var collection in this) { - var (settings, _) = collection[mod.Index]; + var (settings, _) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) collection.Counters.IncrementChange(); } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index e003ad6b..5e361bde 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,7 +1,6 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -63,10 +62,10 @@ public class InheritanceManager : IDisposable, IService if (ReferenceEquals(potentialParent, potentialInheritor)) return ValidInheritance.Self; - if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent)) + if (potentialInheritor.Inheritance.DirectlyInheritsFrom.Contains(potentialParent)) return ValidInheritance.Contained; - if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor))) + if (potentialParent.Inheritance.FlatHierarchy.Any(c => ReferenceEquals(c, potentialInheritor))) return ValidInheritance.Circle; return ValidInheritance.Valid; @@ -83,24 +82,22 @@ public class InheritanceManager : IDisposable, IService /// Remove an existing inheritance from a collection. public void RemoveInheritance(ModCollection inheritor, int idx) { - var parent = inheritor.DirectlyInheritsFrom[idx]; - ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); - ((List)parent.DirectParentOf).Remove(inheritor); + var parent = inheritor.Inheritance.RemoveInheritanceAt(inheritor, idx); _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); + RecurseInheritanceChanges(inheritor, true); Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.AnonymizedName} inheritances."); } /// Order in the inheritance list is relevant. public void MoveInheritance(ModCollection inheritor, int from, int to) { - if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) + if (!inheritor.Inheritance.MoveInheritance(inheritor, from, to)) return; _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); + RecurseInheritanceChanges(inheritor, true); Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}."); } @@ -110,15 +107,15 @@ public class InheritanceManager : IDisposable, IService if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid) return false; - ((List)inheritor.DirectlyInheritsFrom).Add(parent); - ((List)parent.DirectParentOf).Add(inheritor); + inheritor.Inheritance.AddInheritance(inheritor, parent); if (invokeEvent) { _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); } + RecurseInheritanceChanges(inheritor, invokeEvent); + Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances."); return true; } @@ -131,11 +128,11 @@ public class InheritanceManager : IDisposable, IService { foreach (var collection in _storage) { - if (collection.InheritanceByName == null) + if (collection.Inheritance.ConsumeNames() is not { } byName) continue; var changes = false; - foreach (var subCollectionName in collection.InheritanceByName) + foreach (var subCollectionName in byName) { if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection)) { @@ -143,7 +140,8 @@ public class InheritanceManager : IDisposable, IService continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", + Penumbra.Messager.NotificationMessage( + $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) @@ -153,7 +151,8 @@ public class InheritanceManager : IDisposable, IService if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", + Penumbra.Messager.NotificationMessage( + $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else @@ -165,7 +164,6 @@ public class InheritanceManager : IDisposable, IService } } - collection.InheritanceByName = null; if (changes) _saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection)); } @@ -178,20 +176,22 @@ public class InheritanceManager : IDisposable, IService foreach (var c in _storage) { - var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old); + var inheritedIdx = c.Inheritance.DirectlyInheritsFrom.IndexOf(old); if (inheritedIdx >= 0) RemoveInheritance(c, inheritedIdx); - ((List)c.DirectParentOf).Remove(old); + c.Inheritance.RemoveChild(old); } } - private void RecurseInheritanceChanges(ModCollection newInheritor) + private void RecurseInheritanceChanges(ModCollection newInheritor, bool invokeEvent) { - foreach (var inheritor in newInheritor.DirectParentOf) + foreach (var inheritor in newInheritor.Inheritance.DirectlyInheritedBy) { - _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); - RecurseInheritanceChanges(inheritor); + ModCollectionInheritance.UpdateFlattenedInheritance(inheritor); + RecurseInheritanceChanges(inheritor, invokeEvent); + if (invokeEvent) + _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); } } } diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index fe61285d..7db375f7 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -26,12 +26,12 @@ internal static class ModCollectionMigration // Remove all completely defaulted settings from active and inactive mods. for (var i = 0; i < collection.Settings.Count; ++i) { - if (SettingIsDefaultV0(collection.Settings[i])) - ((List)collection.Settings)[i] = null; + if (SettingIsDefaultV0(collection.GetOwnSettings(i))) + collection.Settings.SetAll(i, FullModSettings.Empty); } - foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) - ((Dictionary)collection.UnusedSettings).Remove(key); + foreach (var (key, _) in collection.Settings.Unused.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) + collection.Settings.RemoveUnused(key); return true; } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9b33c1f4..69f82458 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,4 +1,3 @@ -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Mods.Settings; @@ -22,70 +21,74 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(), + new ModCollectionInheritance()); public ModCollectionIdentity Identity; public override string ToString() => Identity.ToString(); - public CollectionCounters Counters; + public readonly ModSettingProvider Settings; + public ModCollectionInheritance Inheritance; + public CollectionCounters Counters; - /// - /// If a ModSetting is null, it can be inherited from other collections. - /// If no collection provides a setting for the mod, it is just disabled. - /// - public readonly IReadOnlyList Settings; - /// Settings for deleted mods will be kept via the mods identifier (directory name). - public readonly IReadOnlyDictionary UnusedSettings; - - /// Inheritances stored before they can be applied. - public IReadOnlyList? InheritanceByName; - - /// Contains all direct parent collections this collection inherits settings from. - public readonly IReadOnlyList DirectlyInheritsFrom; - - /// Contains all direct child collections that inherit from this collection. - public readonly IReadOnlyList DirectParentOf = new List(); - - /// All inherited collections in application order without filtering for duplicates. - public static IEnumerable InheritedCollections(ModCollection collection) - => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection); - - /// - /// Iterate over all collections inherited from in depth-first order. - /// Skip already visited collections to avoid circular dependencies. - /// - public IEnumerable GetFlattenedInheritance() - => InheritedCollections(this).Distinct(); - - /// - /// Obtain the actual settings for a given mod via index. - /// Also returns the collection the settings are taken from. - /// If no collection provides settings for this mod, this collection is returned together with null. - /// - public (ModSettings? Settings, ModCollection Collection) this[Index idx] + public ModSettings? GetOwnSettings(Index idx) { - get + if (Identity.Index <= 0) + return ModSettings.Empty; + + return Settings.Settings[idx].Settings; + } + + public TemporaryModSettings? GetTempSettings(Index idx) + { + if (Identity.Index <= 0) + return null; + + return Settings.Settings[idx].TempSettings; + } + + public (ModSettings? Settings, ModCollection Collection) GetInheritedSettings(Index idx) + { + if (Identity.Index <= 0) + return (ModSettings.Empty, this); + + foreach (var collection in Inheritance.FlatHierarchy) { - if (Identity.Index <= 0) - return (ModSettings.Empty, this); - - foreach (var collection in GetFlattenedInheritance()) - { - var settings = collection.Settings[idx]; - if (settings != null) - return (settings, collection); - } - - return (null, this); + var settings = collection.Settings.Settings[idx].Settings; + if (settings != null) + return (settings, collection); } + + return (null, this); + } + + public (ModSettings? Settings, ModCollection Collection) GetActualSettings(Index idx) + { + if (Identity.Index <= 0) + return (ModSettings.Empty, this); + + // Check temp settings. + var ownTempSettings = Settings.Settings[idx].Resolve(); + if (ownTempSettings != null) + return (ownTempSettings, this); + + // Ignore temp settings for inherited collections. + foreach (var collection in Inheritance.FlatHierarchy.Skip(1)) + { + var settings = collection.Settings.Settings[idx].Settings; + if (settings != null) + return (settings, collection); + } + + return (null, this); } /// Evaluates all settings along the whole inheritance tree. public IEnumerable ActualSettings - => Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings); + => Enumerable.Range(0, Settings.Count).Select(i => GetActualSettings(i).Settings); /// /// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists. @@ -93,9 +96,7 @@ public partial class ModCollection public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, - Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], - UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, Settings.Clone(), Inheritance.Clone()); } /// Constructor for reading from files. @@ -103,11 +104,8 @@ public partial class ModCollection Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(identity.Index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(identity, 0, version, [], [], allSettings) - { - InheritanceByName = inheritances, - }; - ret.ApplyModSettings(saver, mods); + var ret = new ModCollection(identity, 0, version, new ModSettingProvider(allSettings), new ModCollectionInheritance(inheritances)); + ret.Settings.ApplyModSettings(ret, saver, mods); ModCollectionMigration.Migrate(saver, mods, version, ret); return ret; } @@ -116,7 +114,8 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(), + new ModCollectionInheritance()); return ret; } @@ -124,64 +123,18 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, - Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, ModSettingProvider.Empty(modCount), + new ModCollectionInheritance()); } - /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - internal bool AddMod(Mod mod) + private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, ModSettingProvider settings, + ModCollectionInheritance inheritance) { - if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save)) - { - var ret = save.ToSettings(mod, out var settings); - ((List)Settings).Add(settings); - ((Dictionary)UnusedSettings).Remove(mod.ModPath.Name); - return ret; - } - - ((List)Settings).Add(null); - return false; - } - - /// Move settings from the current mod list to the unused mod settings. - internal void RemoveMod(Mod mod) - { - var settings = Settings[mod.Index]; - if (settings != null) - ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); - - ((List)Settings).RemoveAt(mod.Index); - } - - /// Move all settings to unused settings for rediscovery. - internal void PrepareModDiscovery(ModStorage mods) - { - foreach (var (mod, setting) in mods.Zip(Settings).Where(s => s.Second != null)) - ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); - - ((List)Settings).Clear(); - } - - /// - /// Apply all mod settings from unused settings to the current set of mods. - /// Also fixes invalid settings. - /// - internal void ApplyModSettings(SaveService saver, ModStorage mods) - { - ((List)Settings).Capacity = Math.Max(((List)Settings).Capacity, mods.Count); - if (mods.Aggregate(false, (current, mod) => current | AddMod(mod))) - saver.ImmediateSave(new ModCollectionSave(mods, this)); - } - - private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, List appliedSettings, - List inheritsFrom, Dictionary settings) - { - Identity = identity; - Counters = new CollectionCounters(changeCounter); - Settings = appliedSettings; - UnusedSettings = settings; - DirectlyInheritsFrom = inheritsFrom; - foreach (var c in DirectlyInheritsFrom) - ((List)c.DirectParentOf).Add(this); + Identity = identity; + Counters = new CollectionCounters(changeCounter); + Settings = settings; + Inheritance = inheritance; + ModCollectionInheritance.UpdateChildren(this); + ModCollectionInheritance.UpdateFlattenedInheritance(this); } } diff --git a/Penumbra/Collections/ModCollectionInheritance.cs b/Penumbra/Collections/ModCollectionInheritance.cs new file mode 100644 index 00000000..151ed7db --- /dev/null +++ b/Penumbra/Collections/ModCollectionInheritance.cs @@ -0,0 +1,92 @@ +using OtterGui.Filesystem; + +namespace Penumbra.Collections; + +public struct ModCollectionInheritance +{ + public IReadOnlyList? InheritanceByName { get; private set; } + private readonly List _directlyInheritsFrom = []; + private readonly List _directlyInheritedBy = []; + private readonly List _flatHierarchy = []; + + public ModCollectionInheritance() + { } + + private ModCollectionInheritance(List inheritsFrom) + => _directlyInheritsFrom = [.. inheritsFrom]; + + public ModCollectionInheritance(IReadOnlyList byName) + => InheritanceByName = byName; + + public ModCollectionInheritance Clone() + => new(_directlyInheritsFrom); + + public IEnumerable Identifiers + => InheritanceByName ?? _directlyInheritsFrom.Select(c => c.Identity.Identifier); + + public IReadOnlyList? ConsumeNames() + { + var ret = InheritanceByName; + InheritanceByName = null; + return ret; + } + + public static void UpdateChildren(ModCollection parent) + { + foreach (var inheritance in parent.Inheritance.DirectlyInheritsFrom) + inheritance.Inheritance._directlyInheritedBy.Add(parent); + } + + public void AddInheritance(ModCollection inheritor, ModCollection newParent) + { + _directlyInheritsFrom.Add(newParent); + newParent.Inheritance._directlyInheritedBy.Add(inheritor); + UpdateFlattenedInheritance(inheritor); + } + + public ModCollection RemoveInheritanceAt(ModCollection inheritor, int idx) + { + var parent = DirectlyInheritsFrom[idx]; + _directlyInheritsFrom.RemoveAt(idx); + parent.Inheritance._directlyInheritedBy.Remove(parent); + UpdateFlattenedInheritance(inheritor); + return parent; + } + + public bool MoveInheritance(ModCollection inheritor, int from, int to) + { + if (!_directlyInheritsFrom.Move(from, to)) + return false; + + UpdateFlattenedInheritance(inheritor); + return true; + } + + public void RemoveChild(ModCollection child) + => _directlyInheritedBy.Remove(child); + + /// Contains all direct parent collections this collection inherits settings from. + public readonly IReadOnlyList DirectlyInheritsFrom + => _directlyInheritsFrom; + + /// Contains all direct child collections that inherit from this collection. + public readonly IReadOnlyList DirectlyInheritedBy + => _directlyInheritedBy; + + /// + /// Iterate over all collections inherited from in depth-first order. + /// Skip already visited collections to avoid circular dependencies. + /// + public readonly IReadOnlyList FlatHierarchy + => _flatHierarchy; + + public static void UpdateFlattenedInheritance(ModCollection parent) + { + parent.Inheritance._flatHierarchy.Clear(); + parent.Inheritance._flatHierarchy.AddRange(InheritedCollections(parent).Distinct()); + } + + /// All inherited collections in application order without filtering for duplicates. + private static IEnumerable InheritedCollections(ModCollection parent) + => parent.Inheritance.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(parent); +} diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 6e1b51ac..4c41a28c 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -32,19 +32,19 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteValue(modCollection.Identity.Identifier); j.WritePropertyName(nameof(ModCollectionIdentity.Name)); j.WriteValue(modCollection.Identity.Name); - j.WritePropertyName(nameof(ModCollection.Settings)); + j.WritePropertyName("Settings"); // Write all used and unused settings by mod directory name. j.WriteStartObject(); - var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count); + var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.Settings.Unused.Count); for (var i = 0; i < modCollection.Settings.Count; ++i) { - var settings = modCollection.Settings[i]; + var settings = modCollection.GetOwnSettings(i); if (settings != null) list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i]))); } - list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); + list.AddRange(modCollection.Settings.Unused.Select(kvp => (kvp.Key, kvp.Value))); list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); foreach (var (modDir, settings) in list) @@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identity.Identifier)); + x.Serialize(j, modCollection.Inheritance.Identifiers); j.WriteEndObject(); } @@ -82,7 +82,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject() ?? string.Empty; id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. - settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; + settings = obj["Settings"]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; return true; } diff --git a/Penumbra/Collections/ModSettingProvider.cs b/Penumbra/Collections/ModSettingProvider.cs new file mode 100644 index 00000000..3bf2f949 --- /dev/null +++ b/Penumbra/Collections/ModSettingProvider.cs @@ -0,0 +1,98 @@ +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; +using Penumbra.Services; + +namespace Penumbra.Collections; + +public readonly struct ModSettingProvider +{ + private ModSettingProvider(IEnumerable settings, Dictionary unusedSettings) + { + _settings = settings.Select(s => s.DeepCopy()).ToList(); + _unused = unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); + } + + public ModSettingProvider() + { } + + public static ModSettingProvider Empty(int count) + => new(Enumerable.Repeat(FullModSettings.Empty, count), []); + + public ModSettingProvider(Dictionary allSettings) + => _unused = allSettings; + + private readonly List _settings = []; + + /// Settings for deleted mods will be kept via the mods identifier (directory name). + private readonly Dictionary _unused = []; + + public int Count + => _settings.Count; + + public bool RemoveUnused(string key) + => _unused.Remove(key); + + internal void Set(Index index, ModSettings? settings) + => _settings[index] = _settings[index] with { Settings = settings }; + + internal void SetTemporary(Index index, TemporaryModSettings? settings) + => _settings[index] = _settings[index] with { TempSettings = settings }; + + internal void SetAll(Index index, FullModSettings settings) + => _settings[index] = settings; + + public IReadOnlyList Settings + => _settings; + + public IReadOnlyDictionary Unused + => _unused; + + public ModSettingProvider Clone() + => new(_settings, _unused); + + /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. + internal bool AddMod(Mod mod) + { + if (_unused.Remove(mod.ModPath.Name, out var save)) + { + var ret = save.ToSettings(mod, out var settings); + _settings.Add(new FullModSettings(settings)); + return ret; + } + + _settings.Add(FullModSettings.Empty); + return false; + } + + /// Move settings from the current mod list to the unused mod settings. + internal void RemoveMod(Mod mod) + { + var settings = _settings[mod.Index]; + if (settings.Settings != null) + _unused[mod.ModPath.Name] = new ModSettings.SavedSettings(settings.Settings, mod); + + _settings.RemoveAt(mod.Index); + } + + /// Move all settings to unused settings for rediscovery. + internal void PrepareModDiscovery(ModStorage mods) + { + foreach (var (mod, setting) in mods.Zip(_settings).Where(s => s.Second.Settings != null)) + _unused[mod.ModPath.Name] = new ModSettings.SavedSettings(setting.Settings!, mod); + + _settings.Clear(); + } + + /// + /// Apply all mod settings from unused settings to the current set of mods. + /// Also fixes invalid settings. + /// + internal void ApplyModSettings(ModCollection parent, SaveService saver, ModStorage mods) + { + _settings.Capacity = Math.Max(_settings.Capacity, mods.Count); + var settings = this; + if (mods.Aggregate(false, (current, mod) => current | settings.AddMod(mod))) + saver.ImmediateSave(new ModCollectionSave(mods, parent)); + } +} diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 61946978..dee46e32 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -606,7 +606,7 @@ public class CommandHandler : IDisposable, IApiService private bool HandleModState(int settingState, ModCollection collection, Mod mod) { - var settings = collection.Settings[mod.Index]; + var settings = collection.GetOwnSettings(mod.Index); switch (settingState) { case 0: diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 088527ca..4fa13e1f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -98,6 +98,6 @@ public class ResourceNode : ICloneable public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag) { public UiData PrependName(string prefix) - => Name == null ? this : new UiData(prefix + Name, IconFlag); + => Name == null ? this : this with { Name = prefix + Name }; } } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 876fe12f..c06af9c7 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -74,6 +74,9 @@ public class ModMetaEditor( dict.ClearForDefault(); var count = 0; + foreach (var value in clone.GlobalEqp) + dict.TryAdd(value); + foreach (var (key, value) in clone.Imc) { var defaultEntry = ImcChecker.GetDefaultEntry(key, false); diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs index 73d0272b..59cd5d71 100644 --- a/Penumbra/Mods/ModSelection.cs +++ b/Penumbra/Mods/ModSelection.cs @@ -36,9 +36,16 @@ public class ModSelection : EventWrapper _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); } - public ModSettings Settings { get; private set; } = ModSettings.Empty; - public ModCollection Collection { get; private set; } = ModCollection.Empty; - public Mod? Mod { get; private set; } + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + public ModSettings? OwnSettings { get; private set; } + + public bool IsTemporary + => OwnSettings != Settings; + + public TemporaryModSettings? AsTemporarySettings + => Settings as TemporaryModSettings; public void SelectMod(Mod? mod) @@ -83,12 +90,14 @@ public class ModSelection : EventWrapper { if (Mod == null) { - Settings = ModSettings.Empty; - Collection = ModCollection.Empty; + Settings = ModSettings.Empty; + Collection = ModCollection.Empty; + OwnSettings = null; } else { - (var settings, Collection) = _collections.Current[Mod.Index]; + (var settings, Collection) = _collections.Current.GetActualSettings(Mod.Index); + OwnSettings = _collections.Current.GetOwnSettings(Mod.Index); Settings = settings ?? ModSettings.Empty; } } diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 25e4805d..671fba4d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -12,7 +12,7 @@ namespace Penumbra.Mods.Settings; public class ModSettings { public static readonly ModSettings Empty = new(); - public SettingList Settings { get; private init; } = []; + public SettingList Settings { get; internal init; } = []; public ModPriority Priority { get; set; } public bool Enabled { get; set; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a2594145..69dfe3e8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -245,7 +245,7 @@ public class Penumbra : IDalamudPlugin void PrintCollection(ModCollection c, CollectionCache _) => sb.Append( - $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); + $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.Inheritance.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index f58eb891..9fe8c420 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -240,7 +240,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu if (jObject["Name"]?.ToObject() == ForcedCollection) continue; - jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); + jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); File.WriteAllText(collection.FullName, jObject.ToString()); } catch (Exception e) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 3f7f2f6c..b0029f08 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -737,7 +737,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (collectionType is not CollectionType.Current || _mod == null || newCollection == null) return; - UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); + UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.GetInheritedSettings(_mod.Index).Settings : null); } private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) @@ -754,7 +754,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (collection != _collectionManager.Active.Current || _mod == null) return; - UpdateMod(_mod, collection[_mod.Index].Settings); + UpdateMod(_mod, collection.GetInheritedSettings(_mod.Index).Settings); _swapData.LoadMod(_mod, _modSettings); _dirty = true; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 1a4065bb..02e945f3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -101,7 +101,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, _activeCollections.Current.GetInheritedSettings(mod.Index).Settings); UpdateModels(); _forceTextureStartPath = true; }); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cab34b10..8b41b105 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -15,6 +15,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; @@ -497,7 +498,7 @@ public sealed class CollectionPanel( ImGui.Separator(); var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing(); - if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0) + if (_inUseCache.Count == 0 && collection.Inheritance.DirectlyInheritedBy.Count == 0) { ImGui.Dummy(Vector2.One); using var f = _nameFont.Push(); @@ -559,7 +560,7 @@ public sealed class CollectionPanel( private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth) { - if (collection.DirectParentOf.Count <= 0) + if (collection.Inheritance.DirectlyInheritedBy.Count <= 0) return; using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) @@ -570,11 +571,11 @@ public sealed class CollectionPanel( using var f = _nameFont.Push(); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); - ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0); + ImGuiUtil.DrawTextButton(Name(collection.Inheritance.DirectlyInheritedBy[0]), Vector2.Zero, 0); var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2 + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X; - foreach (var parent in collection.DirectParentOf.Skip(1)) + foreach (var parent in collection.Inheritance.DirectlyInheritedBy.Skip(1)) { var name = Name(parent); var size = ImGui.CalcTextSize(name).X; @@ -602,7 +603,7 @@ public sealed class CollectionPanel( ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection[m.Index])) + foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection.GetInheritedSettings(m.Index))) .Where(t => t.Item2.Settings != null) .OrderBy(t => t.m.Name)) { @@ -625,12 +626,12 @@ public sealed class CollectionPanel( private void DrawInactiveSettingsList(ModCollection collection) { - if (collection.UnusedSettings.Count == 0) + if (collection.Settings.Unused.Count == 0) return; ImGui.Dummy(Vector2.One); - var text = collection.UnusedSettings.Count > 1 - ? $"Clear all {collection.UnusedSettings.Count} unused settings from deleted mods." + var text = collection.Settings.Unused.Count > 1 + ? $"Clear all {collection.Settings.Unused.Count} unused settings from deleted mods." : "Clear the currently unused setting from a deleted mods."; if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) _collections.CleanUnavailableSettings(collection); @@ -638,7 +639,7 @@ public sealed class CollectionPanel( ImGui.Dummy(Vector2.One); var size = new Vector2(ImGui.GetContentRegionAvail().X, - Math.Min(10, collection.UnusedSettings.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + Math.Min(10, collection.Settings.Unused.Count + 1) * ImGui.GetFrameHeightWithSpacing()); using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); if (!table) return; @@ -650,7 +651,7 @@ public sealed class CollectionPanel( ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); string? delete = null; - foreach (var (name, settings) in collection.UnusedSettings.OrderBy(n => n.Key)) + foreach (var (name, settings) in collection.Settings.Unused.OrderBy(n => n.Key)) { using var id = ImRaii.PushId(name); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index a4d60b13..ce3cc3cb 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -107,7 +107,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService var lineEnd = lineStart; // Skip the collection itself. - foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1)) + foreach (var inheritance in collection.Inheritance.FlatHierarchy.Skip(1)) { // Draw the child, already seen collections are colored as conflicts. using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), @@ -150,7 +150,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService DrawInheritedChildren(collection); else // We still want to keep track of conflicts. - _seenInheritedCollections.UnionWith(collection.GetFlattenedInheritance()); + _seenInheritedCollections.UnionWith(collection.Inheritance.FlatHierarchy); } /// Draw the list box containing the current inheritance information. @@ -163,7 +163,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService _seenInheritedCollections.Clear(); _seenInheritedCollections.Add(_active.Current); - foreach (var collection in _active.Current.DirectlyInheritsFrom.ToList()) + foreach (var collection in _active.Current.Inheritance.DirectlyInheritsFrom.ToList()) DrawInheritance(collection); } @@ -180,7 +180,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); } /// @@ -244,7 +244,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); _newInheritance ??= _collections.FirstOrDefault(c - => c != _active.Current && !_active.Current.DirectlyInheritsFrom.Contains(c)) + => c != _active.Current && !_active.Current.Inheritance.DirectlyInheritsFrom.Contains(c)) ?? ModCollection.Empty; using var combo = ImRaii.Combo("##newInheritance", Name(_newInheritance)); if (!combo) @@ -271,8 +271,8 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (_movedInheritance != null) { - var idx1 = _active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance); - var idx2 = _active.Current.DirectlyInheritsFrom.IndexOf(collection); + var idx1 = _active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(_movedInheritance); + var idx2 = _active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -302,7 +302,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(collection), -1); + _inheritanceAction = (_active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(collection), -1); else _newCurrentCollection = collection; } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0781312c..4607434c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -201,7 +201,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { @@ -650,14 +664,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector kvp.Key)) { ImUtf8.DrawTableColumn($"{id:D6}"); ImUtf8.DrawTableColumn(name.Span); } - } } } } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index c226098d..8b4913c8 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -82,7 +82,7 @@ public class ModsTab( + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); + + $"{string.Join(", ", _activeCollections.Current.Inheritance.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); } } From 282189ef6dc47edd8135a2f4811721b5d5ca032f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 27 Dec 2024 17:51:17 +0100 Subject: [PATCH 506/865] Current State. --- Penumbra.Api | 2 +- .../Cache/CollectionCacheManager.cs | 3 + .../Collections/Manager/CollectionEditor.cs | 19 ++++- Penumbra/Mods/ModSelection.cs | 17 ++-- Penumbra/Mods/Settings/ModSettings.cs | 1 + .../Mods/Settings/TemporaryModSettings.cs | 17 ++++ Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 29 ++++--- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 81 ++++++++++++------- 9 files changed, 115 insertions(+), 56 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 97e9f427..fdda2054 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 97e9f427406f82a59ddef764b44ecea654a51623 +Subproject commit fdda2054c26a30111ac55984ed6efde7f7214b68 diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 839c0376..27b969c2 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -333,6 +333,9 @@ public class CollectionCacheManager : IDisposable, IService cache.ReloadMod(mod, true); break; + case ModSettingChange.TemporarySetting: + cache.ReloadMod(mod!, true); + break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: FullRecalculation(collection); diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 66578a95..b456686e 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -88,7 +88,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// /// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. - /// /// If the mod is currently inherited, stop the inheritance. + /// If the mod is currently inherited, stop the inheritance. /// public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { @@ -103,6 +103,18 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0) + { + key = settings?.Lock ?? key; + var old = collection.GetTempSettings(mod.Index); + if (old != null && old.Lock != 0 && old.Lock != key) + return false; + + collection.Settings.SetTemporary(mod.Index, settings); + InvokeChange(collection, ModSettingChange.TemporarySetting, mod, Setting.Indefinite, 0); + return true; + } + /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName) { @@ -168,7 +180,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu if (inherit == (settings == null)) return false; - ModSettings? settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + var settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); collection.Settings.Set(mod.Index, settings1); return true; } @@ -179,7 +191,8 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu { saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); - RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); + if (type is not ModSettingChange.TemporarySetting) + RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); } /// Trigger changes in all inherited collections. diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs index 59cd5d71..b728bd00 100644 --- a/Penumbra/Mods/ModSelection.cs +++ b/Penumbra/Mods/ModSelection.cs @@ -36,17 +36,11 @@ public class ModSelection : EventWrapper _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); } - public ModSettings Settings { get; private set; } = ModSettings.Empty; - public ModCollection Collection { get; private set; } = ModCollection.Empty; - public Mod? Mod { get; private set; } - public ModSettings? OwnSettings { get; private set; } - - public bool IsTemporary - => OwnSettings != Settings; - - public TemporaryModSettings? AsTemporarySettings - => Settings as TemporaryModSettings; - + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + public ModSettings? OwnSettings { get; private set; } + public TemporaryModSettings? TemporarySettings { get; private set; } public void SelectMod(Mod? mod) { @@ -98,6 +92,7 @@ public class ModSelection : EventWrapper { (var settings, Collection) = _collections.Current.GetActualSettings(Mod.Index); OwnSettings = _collections.Current.GetOwnSettings(Mod.Index); + TemporarySettings = _collections.Current.GetTempSettings(Mod.Index); Settings = settings ?? ModSettings.Empty; } } diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 671fba4d..0420ee86 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -12,6 +12,7 @@ namespace Penumbra.Mods.Settings; public class ModSettings { public static readonly ModSettings Empty = new(); + public SettingList Settings { get; internal init; } = []; public ModPriority Priority { get; set; } public bool Enabled { get; set; } diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index a0cdc2bb..27987fa6 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -5,4 +5,21 @@ public sealed class TemporaryModSettings : ModSettings public string Source = string.Empty; public int Lock = 0; public bool ForceInherit; + + // Create default settings for a given mod. + public static TemporaryModSettings DefaultSettings(Mod mod, string source, int key = 0) + => new() + { + Enabled = false, + Source = source, + Lock = key, + Priority = ModPriority.Default, + Settings = SettingList.Default(mod), + }; +} + +public static class ModSettingsExtensions +{ + public static bool IsTemporary(this ModSettings? settings) + => settings is TemporaryModSettings; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index b0029f08..8f1ed8d6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -742,7 +742,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) { - if (collection != _collectionManager.Active.Current || mod != _mod) + if (collection != _collectionManager.Active.Current || mod != _mod || type is ModSettingChange.TemporarySetting) return; _swapData.LoadMod(_mod, _modSettings); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 0389730d..fbead9c3 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,8 +1,9 @@ +using ImGuiNET; using OtterGui.Custom; namespace Penumbra.UI.Classes; -public enum ColorId +public enum ColorId : short { EnabledMod, DisabledMod, @@ -10,6 +11,7 @@ public enum ColorId InheritedMod, InheritedDisabledMod, NewMod, + NewModTint, ConflictingMod, HandledConflictMod, FolderExpanded, @@ -31,10 +33,8 @@ public enum ColorId ResTreeNonNetworked, PredefinedTagAdd, PredefinedTagRemove, - TemporaryEnabledMod, - TemporaryDisabledMod, - TemporaryInheritedMod, - TemporaryInheritedDisabledMod, + TemporaryModSettingsTint, + NoTint, } public static class Colors @@ -52,6 +52,18 @@ public static class Colors public const uint ReniColorHovered = CustomGui.ReniColorHovered; public const uint ReniColorActive = CustomGui.ReniColorActive; + public static uint Tinted(this ColorId color, ColorId tint) + { + var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); + var value = ImGui.ColorConvertU32ToFloat4(color.Value()); + var negAlpha = 1 - tintValue.W; + var newAlpha = negAlpha * value.W + tintValue.W; + var newR = (negAlpha * value.W * value.X + tintValue.W * tintValue.X) / newAlpha; + var newG = (negAlpha * value.W * value.Y + tintValue.W * tintValue.Y) / newAlpha; + var newB = (negAlpha * value.W * value.Z + tintValue.W * tintValue.Z) / newAlpha; + return ImGui.ColorConvertFloat4ToU32(new Vector4(newR, newG, newB, newAlpha)); + } + public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { @@ -83,10 +95,9 @@ public static class Colors ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), - ColorId.TemporaryEnabledMod => ( 0xFFFFC0A0, "Mod Enabled By Temporary Settings", "A mod that is enabled by temporary settings in the currently selected collection." ), - ColorId.TemporaryDisabledMod => ( 0xFFB08070, "Mod Disabled By Temporary Settings", "A mod that is disabled by temporary settings in the currently selected collection." ), - ColorId.TemporaryInheritedMod => ( 0xFFE8FFB0, "Mod Enabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), - ColorId.TemporaryInheritedDisabledMod => ( 0xFF90A080, "Mod Disabled By Temporary Inheritance", "A mod that is forced to inherit by temporary settings in the currently selected collection." ), + ColorId.TemporaryModSettingsTint => ( 0x30FF0000, "Mod with Temporary Settings", "A mod that has temporary settings. This color is used as a tint for the regular state colors." ), + ColorId.NewModTint => ( 0x8000FF00, "New Mod Tint", "A mod that was newly imported or created during this session and has not been enabled yet. This color is used as a tint for the regular state colors."), + ColorId.NoTint => ( 0x00000000, "No Tint", "The default tint for all mods."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 4607434c..c3cb211c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -62,6 +62,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector SetQuickMove(f, 1, _config.QuickMoveFolder2, s => { _config.QuickMoveFolder2 = s; _config.Save(); }), 120); SubscribeRightClickFolder(f => SetQuickMove(f, 2, _config.QuickMoveFolder3, s => { _config.QuickMoveFolder3 = s; _config.Save(); }), 130); SubscribeRightClickLeaf(ToggleLeafFavorite); + SubscribeRightClickLeaf(RemoveTemporarySettings); + SubscribeRightClickLeaf(DisableTemporarily); SubscribeRightClickLeaf(l => QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); SubscribeRightClickMain(ClearDefaultImportFolder, 100); SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); @@ -194,7 +196,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf leaf, in ModState state, bool selected) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Tinted(state.Tint)) .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); using var id = ImRaii.PushId(leaf.Value.Index); ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); @@ -264,6 +266,23 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) + { + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + if (tempSettings is { Lock: 0 }) + if (ImUtf8.MenuItem("Remove Temporary Settings")) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); + } + + private void DisableTemporarily(FileSystem.Leaf mod) + { + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + if (tempSettings == null || tempSettings.Lock == 0) + if (ImUtf8.MenuItem("Disable Temporarily")) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, + TemporaryModSettings.DefaultSettings(mod.Value, "User Context-Menu")); + } + private void SetDefaultImportFolder(ModFileSystem.Folder folder) { if (!ImGui.MenuItem("Set As Default Import Folder")) @@ -392,8 +411,6 @@ public sealed class ModFileSystemSelector : FileSystemSelector !_filter.IsVisible(leaf); /// Only get the text color for a mod if no filters are set. - private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) + private (ColorId Color, ColorId Tint) GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) { - if (_modManager.IsNew(mod)) - return ColorId.NewMod; + var tint = settings.IsTemporary() + ? ColorId.TemporaryModSettingsTint + : _modManager.IsNew(mod) + ? ColorId.NewModTint + : ColorId.NoTint; + if (settings.IsTemporary()) + tint = ColorId.TemporaryModSettingsTint; if (settings == null) - return ColorId.UndefinedMod; + return (ColorId.UndefinedMod, tint); if (!settings.Enabled) - return collection != _collectionManager.Active.Current - ? ColorId.InheritedDisabledMod - : settings is TemporaryModSettings - ? ColorId.TemporaryDisabledMod - : ColorId.DisabledMod; - - if (settings is TemporaryModSettings) - return ColorId.TemporaryEnabledMod; + return (collection != _collectionManager.Active.Current + ? ColorId.InheritedDisabledMod + : ColorId.DisabledMod, tint); var conflicts = _collectionManager.Active.Current.Conflicts(mod); if (conflicts.Count == 0) - return collection != _collectionManager.Active.Current ? ColorId.InheritedMod : ColorId.EnabledMod; + return (collection != _collectionManager.Active.Current ? ColorId.InheritedMod : ColorId.EnabledMod, tint); - return conflicts.Any(c => !c.Solved) + return (conflicts.Any(c => !c.Solved) ? ColorId.ConflictingMod - : ColorId.HandledConflictMod; + : ColorId.HandledConflictMod, tint); } private bool CheckStateFilters(Mod mod, ModSettings? settings, ModCollection collection, ref ModState state) @@ -627,6 +645,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { @@ -664,14 +686,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Sun, 29 Dec 2024 00:05:36 +0100 Subject: [PATCH 507/865] Current State. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 141 +----------------- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/TemporaryApi.cs | 140 ++++++++++++++++- Penumbra/Api/IpcProviders.cs | 8 +- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 40 +++++ .../Collections/Manager/CollectionEditor.cs | 9 +- .../SchedulerResourceManagementService.cs | 2 +- .../Mods/Settings/TemporaryModSettings.cs | 16 ++ Penumbra/UI/Classes/Colors.cs | 24 ++- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 89 +++++++---- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 111 ++++++++++---- Penumbra/UI/Tabs/Debug/DebugTab.cs | 8 +- 14 files changed, 381 insertions(+), 213 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index fdda2054..882b778e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit fdda2054c26a30111ac55984ed6efde7f7214b68 +Subproject commit 882b778e78bb0806dd7d38e8b3670ff138a84a31 diff --git a/Penumbra.String b/Penumbra.String index dd83f972..0647fbc5 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936 +Subproject commit 0647fbc5017ef9ced3f3ce1c2496eefd57c5b7a8 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 4027975b..b78523d3 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,5 +1,4 @@ using OtterGui; -using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -25,20 +24,18 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private readonly CollectionManager _collectionManager; private readonly CollectionEditor _collectionEditor; private readonly CommunicatorService _communicator; - private readonly ApiHelpers _helpers; public ModSettingsApi(CollectionResolver collectionResolver, ModManager modManager, CollectionManager collectionManager, CollectionEditor collectionEditor, - CommunicatorService communicator, ApiHelpers helpers) + CommunicatorService communicator) { _collectionResolver = collectionResolver; _modManager = modManager; _collectionManager = collectionManager; _collectionEditor = collectionEditor; _communicator = communicator; - _helpers = helpers; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); @@ -209,142 +206,6 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } - public PenumbraApiEc SetTemporaryModSetting(Guid collectionId, string modDirectory, string modName, bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled, - "Priority", priority, "Options", options, "Source", source, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return SetTemporaryModSetting(args, collection, modDirectory, modName, enabled, priority, options, source, key); - } - - public PenumbraApiEc TemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - return PenumbraApiEc.Success; - } - - private PenumbraApiEc SetTemporaryModSetting(in LazyString args, ModCollection collection, string modDirectory, string modName, - bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - - if (collection.GetTempSettings(mod.Index) is { } settings && settings.Lock != 0 && settings.Lock != key) - return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); - - settings = new TemporaryModSettings - { - Enabled = enabled, - Priority = new ModPriority(priority), - Lock = key, - Source = source, - Settings = SettingList.Default(mod), - }; - - foreach (var (groupName, optionNames) in options) - { - var groupIdx = mod.Groups.IndexOf(g => g.Name == groupName); - if (groupIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - - var setting = Setting.Zero; - switch (mod.Groups[groupIdx]) - { - case { Behaviour: GroupDrawBehaviour.SingleSelection } single: - { - var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting = Setting.Single(optionIdx); - break; - } - case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: - { - foreach (var name in optionNames) - { - var optionIdx = multi.Options.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting |= Setting.Multi(optionIdx); - } - - break; - } - } - - settings.Settings[groupIdx] = setting; - } - - collection.Settings.SetTemporary(mod.Index, settings); - return ApiHelpers.Return(PenumbraApiEc.Success, args); - } - - public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); - } - - private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) - { - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - - if (collection.GetTempSettings(mod.Index) is not { } settings) - return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); - - if (settings.Lock != 0 && settings.Lock != key) - return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); - - collection.Settings.SetTemporary(mod.Index, null); - return ApiHelpers.Return(PenumbraApiEc.Success, args); - } - - public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) - { - return PenumbraApiEc.Success; - } - - public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return RemoveAllTemporaryModSettings(args, collection, key); - } - - public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) - { - return PenumbraApiEc.Success; - } - - private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) - { - var numRemoved = 0; - for (var i = 0; i < collection.Settings.Count; ++i) - { - if (collection.GetTempSettings(i) is { } settings && (settings.Lock == 0 || settings.Lock == key)) - { - collection.Settings.SetTemporary(i, null); - ++numRemoved; - } - } - - return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index eaaf9f38..894b2674 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 3); + => (5, 4); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 201839e7..afddeae8 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -1,8 +1,11 @@ +using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; +using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; using Penumbra.String.Classes; @@ -13,7 +16,9 @@ public class TemporaryApi( ObjectManager objects, ActorManager actors, CollectionManager collectionManager, - TempModManager tempMods) : IPenumbraApiTemporary, IApiService + TempModManager tempMods, + ApiHelpers apiHelpers, + ModManager modManager) : IPenumbraApiTemporary, IApiService { public Guid CreateTemporaryCollection(string name) => tempCollections.CreateTemporaryCollection(name); @@ -125,6 +130,139 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } + + public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); + } + + public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); + } + + private PenumbraApiEc SetTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, + bool inherit, bool enabled, int priority, IReadOnlyDictionary> options, string source, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingImpossible, args); + + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key)) + if (collection.GetTempSettings(mod.Index) is { } oldSettings && oldSettings.Lock != 0 && oldSettings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + var newSettings = new TemporaryModSettings() + { + ForceInherit = inherit, + Enabled = enabled, + Priority = new ModPriority(priority), + Lock = key, + Source = source, + Settings = SettingList.Default(mod), + }; + + + foreach (var (groupName, optionNames) in options) + { + var ec = ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIdx, out var setting); + if (ec != PenumbraApiEc.Success) + return ApiHelpers.Return(ec, args); + + newSettings.Settings[groupIdx] = setting; + } + + if (collectionManager.Editor.SetTemporarySettings(collection, mod, newSettings, key)) + return ApiHelpers.Return(PenumbraApiEc.Success, args); + + // This should not happen since all error cases had been checked before. + return ApiHelpers.Return(PenumbraApiEc.UnknownError, args); + } + + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is null) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (!collectionManager.Editor.SetTemporarySettings(collection, mod, null, key)) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is not null + && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) + ++numRemoved; + } + + return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); + } + + /// /// Convert a dictionary of strings to a dictionary of game paths to full paths. /// Only returns true if all paths can successfully be converted and added. diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 861225fa..6f3b2c38 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -63,7 +63,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ApiVersion.Provider(pi, api), new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility - new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility + new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), @@ -97,6 +97,12 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary), IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary), IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary), + IpcSubscribers.SetTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.SetTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index f3c23831..2364dddf 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -33,6 +33,7 @@ public class TemporaryIpcTester( private string _tempCollectionName = string.Empty; private string _tempCollectionGuidName = string.Empty; private string _tempModName = string.Empty; + private string _modDirectory = string.Empty; private string _tempGamePath = "test/game/path.mtrl"; private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; @@ -50,6 +51,7 @@ public class TemporaryIpcTester( ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); @@ -121,6 +123,44 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); if (ImGui.Button("Remove##ModAll")) _lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue); + + IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); + if (ImUtf8.Button("Set##SetTemporary"u8)) + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + "IPC Tester", 1337); + + IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); + if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + "IPC Tester", 1337); + + IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); + if (ImUtf8.Button("Remove##RemoveTemporary"u8)) + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8)) + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1338); + + IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection"); + if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8)) + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8)) + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1338); + + IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection"); + if (ImUtf8.Button("Remove##RemoveAllTemporary"u8)) + _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporary"u8)) + _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1338); + + IpcTester.DrawIntro(RemoveAllTemporaryModSettingsPlayer.Label, "Remove All Temporary Mod Settings from game object Collection"); + if (ImUtf8.Button("Remove##RemoveAllTemporaryPlayer"u8)) + _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8)) + _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338); } public void DrawCollections() diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index b456686e..124f8cf7 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -106,8 +106,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0) { key = settings?.Lock ?? key; - var old = collection.GetTempSettings(mod.Index); - if (old != null && old.Lock != 0 && old.Lock != key) + if (!CanSetTemporarySettings(collection, mod, key)) return false; collection.Settings.SetTemporary(mod.Index, settings); @@ -115,6 +114,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) + { + var old = collection.GetTempSettings(mod.Index); + return old == null || old.Lock == 0 || old.Lock == key; + } + /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName) { diff --git a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs index 1d56fcdb..b7f57a44 100644 --- a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs +++ b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs @@ -69,7 +69,7 @@ public unsafe class SchedulerResourceManagementService : IService, IDisposable if (_actionTmbs.TryGetValue(tmb, out var rowId)) _listedTmbIds[rowId] = tmb; else - Penumbra.Log.Debug($"Action TMB {gamePath} encountered with no corresponding row ID."); + Penumbra.Log.Verbose($"Action TMB {gamePath} encountered with no corresponding row ID."); } [Signature(Sigs.SchedulerResourceManagementInstance, ScanType = ScanType.StaticAddress)] diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index 27987fa6..de4570c5 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -16,6 +16,22 @@ public sealed class TemporaryModSettings : ModSettings Priority = ModPriority.Default, Settings = SettingList.Default(mod), }; + + public TemporaryModSettings() + { } + + public TemporaryModSettings(ModSettings? clone, string source, int key = 0) + { + Source = source; + Lock = key; + ForceInherit = clone == null; + if (clone != null) + { + Enabled = clone.Enabled; + Priority = clone.Priority; + Settings = clone.Settings.Clone(); + } + } } public static class ModSettingsExtensions diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index fbead9c3..4c0d1694 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -56,12 +56,24 @@ public static class Colors { var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); var value = ImGui.ColorConvertU32ToFloat4(color.Value()); - var negAlpha = 1 - tintValue.W; - var newAlpha = negAlpha * value.W + tintValue.W; - var newR = (negAlpha * value.W * value.X + tintValue.W * tintValue.X) / newAlpha; - var newG = (negAlpha * value.W * value.Y + tintValue.W * tintValue.Y) / newAlpha; - var newB = (negAlpha * value.W * value.Z + tintValue.W * tintValue.Z) / newAlpha; - return ImGui.ColorConvertFloat4ToU32(new Vector4(newR, newG, newB, newAlpha)); + return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); + } + + public static unsafe uint Tinted(this ImGuiCol color, ColorId tint) + { + var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); + ref var value = ref *ImGui.GetStyleColorVec4(color); + return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); + } + + private static unsafe Vector4 TintColor(in Vector4 color, in Vector4 tint) + { + var negAlpha = 1 - tint.W; + var newAlpha = negAlpha * color.W + tint.W; + var newR = (negAlpha * color.W * color.X + tint.W * tint.X) / newAlpha; + var newG = (negAlpha * color.W * color.Y + tint.W * tint.Y) / newAlpha; + var newB = (negAlpha * color.W * color.Z + tint.W * tint.Z) / newAlpha; + return new Vector4(newR, newG, newB, newAlpha); } public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index dec77430..527d8bce 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -16,13 +17,19 @@ namespace Penumbra.UI.ModsTab.Groups; public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService { private readonly List<(IModGroup, int)> _blockGroupCache = []; + private bool _temporary; + private bool _locked; + private TemporaryModSettings? _tempSettings; - public void Draw(Mod mod, ModSettings settings) + public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { if (mod.Groups.Count <= 0) return; _blockGroupCache.Clear(); + _tempSettings = tempSettings; + _temporary = tempSettings != null; + _locked = (tempSettings?.Lock ?? 0) != 0; var useDummy = true; foreach (var (group, idx) in mod.Groups.WithIndex()) { @@ -63,22 +70,23 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; + using var id = ImUtf8.PushId(groupIdx); + var selectedOption = setting.AsIndex; + using var disabled = ImRaii.Disabled(_locked); ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); var options = group.Options; - using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) + using (var combo = ImUtf8.Combo(""u8, options[selectedOption].Name)) { if (combo) for (var idx2 = 0; idx2 < options.Count; ++idx2) { id.Push(idx2); var option = options[idx2]; - if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + if (ImUtf8.Selectable(option.Name, idx2 == selectedOption)) SetModSetting(group, groupIdx, Setting.Single(idx2)); if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); + ImUtf8.SelectableHelpMarker(option.Description); id.Pop(); } @@ -86,9 +94,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle ImGui.SameLine(); if (group.Description.Length > 0) - ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + ImUtf8.LabeledHelpMarker(group.Name, group.Description); else - ImGui.TextUnformatted(group.Name); + ImUtf8.Text(group.Name); } /// @@ -97,10 +105,10 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImUtf8.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -108,11 +116,12 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle void DrawOptions() { + using var disabled = ImRaii.Disabled(_locked); for (var idx = 0; idx < group.Options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - if (ImGui.RadioButton(option.Name, selectedOption == idx)) + using var i = ImUtf8.PushId(idx); + var option = options[idx]; + if (ImUtf8.RadioButton(option.Name, selectedOption == idx)) SetModSetting(group, groupIdx, Setting.Single(idx)); if (option.Description.Length <= 0) @@ -130,28 +139,29 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImUtf8.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); + ImUtf8.OpenPopup($"##multi{groupIdx}"); DrawMultiPopup(group, groupIdx, label); return; void DrawOptions() { + using var disabled = ImRaii.Disabled(_locked); for (var idx = 0; idx < options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var enabled = setting.HasFlag(idx); + using var i = ImUtf8.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); - if (ImGui.Checkbox(option.Name, ref enabled)) + if (ImUtf8.Checkbox(option.Name, ref enabled)) SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); if (option.Description.Length > 0) @@ -171,11 +181,12 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle return; ImGui.TextUnformatted(group.Name); + using var disabled = ImRaii.Disabled(_locked); ImGui.Separator(); - if (ImGui.Selectable("Enable All")) + if (ImUtf8.Selectable("Enable All"u8)) SetModSetting(group, groupIdx, Setting.AllBits(group.Options.Count)); - if (ImGui.Selectable("Disable All")) + if (ImUtf8.Selectable("Disable All"u8)) SetModSetting(group, groupIdx, Setting.Zero); } @@ -187,11 +198,11 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } else { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var collapseId = ImUtf8.GetId("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var buttonTextShow = $"Show {options.Count} Options"; var buttonTextHide = $"Hide {options.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + var buttonWidth = Math.Max(ImUtf8.CalcTextSize(buttonTextShow).X, ImUtf8.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); if (shown) @@ -204,22 +215,22 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); - if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) + if (ImUtf8.Button(buttonTextHide, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); ImGui.SetCursorPos(endPos); } else { - var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = options.Max(o => ImUtf8.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; var width = Math.Max(optionWidth, minWidth); - if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) + if (ImUtf8.Button(buttonTextShow, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); } } @@ -228,6 +239,18 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private ModCollection Current => collectionManager.Active.Current; + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) - => collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + { + if (_temporary) + { + _tempSettings!.ForceInherit = false; + _tempSettings!.Settings[groupIdx] = setting; + collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); + } + else + { + collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + } + } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 261f6e92..cf64c00a 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,6 +1,5 @@ using ImGuiNET; using OtterGui.Raii; -using OtterGui; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; @@ -24,6 +23,8 @@ public class ModPanelSettingsTab( : ITab, IUiService { private bool _inherited; + private bool _temporary; + private bool _locked; private int? _currentPriority; public ReadOnlySpan Label @@ -37,11 +38,14 @@ public class ModPanelSettingsTab( public void DrawContent() { - using var child = ImRaii.Child("##settings"); + using var child = ImUtf8.Child("##settings"u8, default); if (!child) return; - _inherited = selection.Collection != collectionManager.Active.Current; + _inherited = selection.Collection != collectionManager.Active.Current; + _temporary = selection.TemporarySettings != null; + _locked = (selection.TemporarySettings?.Lock ?? 0) != 0; + DrawTemporaryWarning(); DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); @@ -54,11 +58,27 @@ public class ModPanelSettingsTab( communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); - modGroupDrawer.Draw(selection.Mod!, selection.Settings); + modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); UiHelpers.DefaultLineSpace(); communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier); } + /// Draw a big tinted bar if the current setting is temporary. + private void DrawTemporaryWarning() + { + if (!_temporary) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiCol.Button.Tinted(ColorId.TemporaryModSettingsTint)); + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + if (ImUtf8.ButtonEx($"These settings are temporary from {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", width, + _locked)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); + + ImUtf8.HoverTooltip("Changing settings in temporary settings will not save them across sessions.\n"u8 + + "You can click this button to remove the temporary settings and return to your normal settings."u8); + } + /// Draw a big red bar if the current setting is inherited. private void DrawInheritedWarning() { @@ -67,22 +87,42 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + if (ImUtf8.ButtonEx($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked)) + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + } + } - ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" - + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); + ImUtf8.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"u8 + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8); } /// Draw a checkbox for the enabled status of the mod. private void DrawEnabledInput() { - var enabled = selection.Settings.Enabled; - if (!ImGui.Checkbox("Enabled", ref enabled)) + var enabled = selection.Settings.Enabled; + using var disabled = ImRaii.Disabled(_locked); + if (!ImUtf8.Checkbox("Enabled"u8, ref enabled)) return; modManager.SetKnown(selection.Mod!); - collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + selection.TemporarySettings!.Enabled = enabled; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + } } /// @@ -91,45 +131,66 @@ public class ModPanelSettingsTab( /// private void DrawPriorityInput() { - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); var settings = selection.Settings; var priority = _currentPriority ?? settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); - if (ImGui.InputInt("##Priority", ref priority, 0, 0)) + using var disabled = ImRaii.Disabled(_locked); + if (ImUtf8.InputScalar("##Priority"u8, ref priority)) _currentPriority = priority; if (new ModPriority(priority).IsHidden) - ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != settings.Priority.Value) - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, - new ModPriority(_currentPriority.Value)); + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + selection.TemporarySettings!.Priority = new ModPriority(_currentPriority.Value); + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, + new ModPriority(_currentPriority.Value)); + } + } _currentPriority = null; } - ImGuiUtil.LabeledHelpMarker("Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."); + ImUtf8.LabeledHelpMarker("Priority"u8, "Mods with a higher number here take precedence before Mods with a lower number.\n"u8 + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8); } /// /// Draw a button to remove the current settings and inherit them instead - /// on the top-right corner of the window/tab. + /// in the top-right corner of the window/tab. /// private void DrawRemoveSettings() { - const string text = "Inherit Settings"; if (_inherited || selection.Settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); - if (ImGui.Button(text)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + ImGui.SameLine(ImGui.GetWindowWidth() - ImUtf8.CalcTextSize("Inherit Settings"u8).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + if (!ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + return; - ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" - + "If no inherited collection has settings for this mod, it will be disabled."); + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = true; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + } } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5fd38d94..c5168109 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -791,19 +791,25 @@ public class DebugTab : Window, ITab, IUiService ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); } + private string _tmbKeyFilter = string.Empty; + private CiByteString _tmbKeyFilterU8 = CiByteString.Empty; + private void DrawActionTmbs() { using var mainTree = TreeNode("Action TMBs"); if (!mainTree) return; + if (ImGui.InputText("Key", ref _tmbKeyFilter, 256)) + _tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty; using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); if (!table) return; var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - var dummy = ImGuiClip.ClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + kvp => kvp.Key.Contains(_tmbKeyFilterU8), p => { ImUtf8.DrawTableColumn($"{p.Value}"); From cff482a2ed1c542935b6912dc14731e50cfb14ad Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 16:36:46 +0100 Subject: [PATCH 508/865] Allow non-locking, negative identifier-locks --- Penumbra/Api/Api/TemporaryApi.cs | 4 ++-- Penumbra/Collections/Manager/CollectionEditor.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index afddeae8..b12ce707 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -163,7 +163,7 @@ public class TemporaryApi( return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key)) - if (collection.GetTempSettings(mod.Index) is { } oldSettings && oldSettings.Lock != 0 && oldSettings.Lock != key) + if (collection.GetTempSettings(mod.Index) is { Lock: > 0 } oldSettings && oldSettings.Lock != key) return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); var newSettings = new TemporaryModSettings() @@ -254,7 +254,7 @@ public class TemporaryApi( var numRemoved = 0; for (var i = 0; i < collection.Settings.Count; ++i) { - if (collection.GetTempSettings(i) is not null + if (collection.GetTempSettings(i) is {} tempSettings && tempSettings.Lock == key && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) ++numRemoved; } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 124f8cf7..437d4e0b 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -117,7 +117,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) { var old = collection.GetTempSettings(mod.Index); - return old == null || old.Lock == 0 || old.Lock == key; + return old is not { Lock: > 0 } || old.Lock == key; } /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 527d8bce..b723978b 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -29,7 +29,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle _blockGroupCache.Clear(); _tempSettings = tempSettings; _temporary = tempSettings != null; - _locked = (tempSettings?.Lock ?? 0) != 0; + _locked = (tempSettings?.Lock ?? 0) > 0; var useDummy = true; foreach (var (group, idx) in mod.Groups.WithIndex()) { diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index c3cb211c..091a2937 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -269,7 +269,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings is { Lock: 0 }) + if (tempSettings is { Lock: <= 0 }) if (ImUtf8.MenuItem("Remove Temporary Settings")) _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); } @@ -277,7 +277,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings == null || tempSettings.Lock == 0) + if (tempSettings is not { Lock: > 0 }) if (ImUtf8.MenuItem("Disable Temporarily")) _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, TemporaryModSettings.DefaultSettings(mod.Value, "User Context-Menu")); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index cf64c00a..60666810 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -44,7 +44,7 @@ public class ModPanelSettingsTab( _inherited = selection.Collection != collectionManager.Active.Current; _temporary = selection.TemporarySettings != null; - _locked = (selection.TemporarySettings?.Lock ?? 0) != 0; + _locked = (selection.TemporarySettings?.Lock ?? 0) > 0; DrawTemporaryWarning(); DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); From 653f6269b7b190363da723739b359b4607e764d6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 16:38:15 +0100 Subject: [PATCH 509/865] Update submodule. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 882b778e..de0f281f 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 882b778e78bb0806dd7d38e8b3670ff138a84a31 +Subproject commit de0f281fbf9d8d9d3aa8463a28025d54877cde8d From dbef1cccb2b0ff89eec53ec64c814126f0a4f839 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 17:10:09 +0100 Subject: [PATCH 510/865] Fix stuff after submodule update. --- Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 12 ++++++------ Penumbra/Mods/Settings/TemporaryModSettings.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 2364dddf..832fea82 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -126,27 +126,27 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); if (ImUtf8.Button("Set##SetTemporary"u8)) - _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337, new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) - _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337, new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); if (ImUtf8.Button("Remove##RemoveTemporary"u8)) - _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1337); + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1337); ImGui.SameLine(); if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8)) - _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1338); + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, 1338); IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection"); if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8)) - _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1337); + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1337); ImGui.SameLine(); if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8)) - _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1338); + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, 1338); IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection"); if (ImUtf8.Button("Remove##RemoveAllTemporary"u8)) diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index de4570c5..425e0348 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -7,10 +7,10 @@ public sealed class TemporaryModSettings : ModSettings public bool ForceInherit; // Create default settings for a given mod. - public static TemporaryModSettings DefaultSettings(Mod mod, string source, int key = 0) + public static TemporaryModSettings DefaultSettings(Mod mod, string source, bool enabled = false, int key = 0) => new() { - Enabled = false, + Enabled = enabled, Source = source, Lock = key, Priority = ModPriority.Default, From a2258e61606474b9fb4d45be226918f5d498ae08 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 31 Dec 2024 17:10:18 +0100 Subject: [PATCH 511/865] Add some temporary context menu things. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 163 ++++++++++--------- 2 files changed, 87 insertions(+), 78 deletions(-) diff --git a/OtterGui b/OtterGui index fcc96daa..fd387218 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fcc96daa02633f673325c14aeea6b6b568924f1e +Subproject commit fd387218d2d2d237075cb35be6ca89eeb53e14e5 diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 091a2937..280956f4 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -62,8 +62,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector SetQuickMove(f, 1, _config.QuickMoveFolder2, s => { _config.QuickMoveFolder2 = s; _config.Save(); }), 120); SubscribeRightClickFolder(f => SetQuickMove(f, 2, _config.QuickMoveFolder3, s => { _config.QuickMoveFolder3 = s; _config.Save(); }), 130); SubscribeRightClickLeaf(ToggleLeafFavorite); - SubscribeRightClickLeaf(RemoveTemporarySettings); - SubscribeRightClickLeaf(DisableTemporarily); + SubscribeRightClickLeaf(DrawTemporaryOptions); SubscribeRightClickLeaf(l => QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); SubscribeRightClickMain(ClearDefaultImportFolder, 100); SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); @@ -135,7 +134,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector m.Extensions.Any(e => ValidModExtensions.Contains(e.ToLowerInvariant())), m => { - ImGui.TextUnformatted($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); + ImUtf8.Text($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); return true; }); base.Draw(width); @@ -198,8 +197,8 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { - if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) + if (ImUtf8.MenuItem(mod.Value.Favorite ? "Remove Favorite"u8 : "Mark as Favorite"u8)) _modManager.DataEditor.ChangeModFavorite(mod.Value, !mod.Value.Favorite); } - private void RemoveTemporarySettings(FileSystem.Leaf mod) + private void DrawTemporaryOptions(FileSystem.Leaf mod) { - var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings is { Lock: <= 0 }) - if (ImUtf8.MenuItem("Remove Temporary Settings")) - _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); - } + const string source = "yourself"; + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + if (tempSettings is { Lock: > 0 }) + return; - private void DisableTemporarily(FileSystem.Leaf mod) - { - var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); - if (tempSettings is not { Lock: > 0 }) - if (ImUtf8.MenuItem("Disable Temporarily")) - _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, - TemporaryModSettings.DefaultSettings(mod.Value, "User Context-Menu")); + if (tempSettings is { Lock: <= 0 } && ImUtf8.MenuItem("Remove Temporary Settings"u8)) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, null); + var actual = _collectionManager.Active.Current.GetActualSettings(mod.Value.Index).Settings; + if (actual?.Enabled is true && ImUtf8.MenuItem("Disable Temporarily"u8)) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, + new TemporaryModSettings(actual, source) { Enabled = false }); + + if (actual is not { Enabled: true } && ImUtf8.MenuItem("Enable Temporarily"u8)) + { + var newSettings = actual is null + ? TemporaryModSettings.DefaultSettings(mod.Value, source, true) + : new TemporaryModSettings(actual, source) { Enabled = true }; + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, newSettings); + } + + if (tempSettings is null && ImUtf8.MenuItem("Turn Temporary"u8)) + _collectionManager.Editor.SetTemporarySettings(_collectionManager.Active.Current, mod.Value, + new TemporaryModSettings(actual, source)); } private void SetDefaultImportFolder(ModFileSystem.Folder folder) { - if (!ImGui.MenuItem("Set As Default Import Folder")) + if (!ImUtf8.MenuItem("Set As Default Import Folder"u8)) return; var newName = folder.FullName(); @@ -298,7 +307,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Add an import mods button that opens a file selector. private void AddImportModButton(Vector2 size) { - var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !_modManager.Valid, true); + var button = ImUtf8.IconButton(FontAwesomeIcon.FileImport, + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files."u8, size, !_modManager.Valid); _tutorial.OpenTutorial(BasicTutorialSteps.ModImport); if (!button) return; @@ -351,14 +359,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector { ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Management"); - ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); + ImUtf8.Text("Mod Management"u8); + ImUtf8.BulletText("You can create empty mods or import mods with the buttons in this row."u8); using var indent = ImRaii.PushIndent(); - ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); - ImGui.BulletText( - "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); + ImUtf8.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."u8); + ImUtf8.BulletText( + "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."u8); indent.Pop(1); - ImGui.BulletText("You can also create empty mod folders and delete mods."); - ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); + ImUtf8.BulletText("You can also create empty mod folders and delete mods."u8); + ImUtf8.BulletText( + "For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."u8); ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Selector"); - ImGui.BulletText("Select a mod to obtain more information or change settings."); - ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); + ImUtf8.Text("Mod Selector"u8); + ImUtf8.BulletText("Select a mod to obtain more information or change settings."u8); + ImUtf8.BulletText("Names are colored according to your config and their current state in the collection:"u8); indent.Push(); - ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."); - ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); - ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(), - "enabled and conflicting with another enabled Mod on the same priority."); - ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."); - ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"); + ImUtf8.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."u8); + ImUtf8.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."u8); + ImUtf8.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."u8); + ImUtf8.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."u8); + ImUtf8.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."u8); + ImUtf8.BulletTextColored(ColorId.HandledConflictMod.Value(), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."u8); + ImUtf8.BulletTextColored(ColorId.ConflictingMod.Value(), + "enabled and conflicting with another enabled Mod on the same priority."u8); + ImUtf8.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."u8); + ImUtf8.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"u8); indent.Pop(1); - ImGui.BulletText("Middle-click a mod to disable it if it is enabled or enable it if it is disabled."); + ImUtf8.BulletText("Middle-click a mod to disable it if it is enabled or enable it if it is disabled."u8); indent.Push(); - ImGui.BulletText( + ImUtf8.BulletText( $"Holding {_config.DeleteModModifier.ForcedModifier(new DoubleModifier(ModifierHotkey.Control, ModifierHotkey.Shift))} while middle-clicking lets it inherit, discarding settings."); indent.Pop(1); - ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); + ImUtf8.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."u8); indent.Push(); - ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); + ImUtf8.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."u8); + ImUtf8.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."u8); indent.Pop(1); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); + ImUtf8.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."u8); indent.Push(); - ImGui.BulletText( - "You can select multiple mods and folders by holding Control while clicking them, and then drag all of them at once."); - ImGui.BulletText( - "Selected mods inside an also selected folder will be ignored when dragging and move inside their folder instead of directly into the target."); + ImUtf8.BulletText( + "You can select multiple mods and folders by holding Control while clicking them, and then drag all of them at once."u8); + ImUtf8.BulletText( + "Selected mods inside an also selected folder will be ignored when dragging and move inside their folder instead of directly into the target."u8); indent.Pop(1); - ImGui.BulletText("Right-clicking a folder opens a context menu."); - ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); - ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); + ImUtf8.BulletText("Right-clicking a folder opens a context menu."u8); + ImUtf8.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."u8); + ImUtf8.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."u8); indent.Push(); - ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); - ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); - ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); + ImUtf8.BulletText("You can enter n:[string] to filter only for names, without path."u8); + ImUtf8.BulletText("You can enter c:[string] to filter for Changed Items instead."u8); + ImUtf8.BulletText("You can enter a:[string] to filter for Mod Authors instead."u8); indent.Pop(1); - ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); + ImUtf8.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."u8); }); } @@ -729,7 +738,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Tue, 31 Dec 2024 17:56:58 +0100 Subject: [PATCH 512/865] Keep enabled and priority at the top of settings, add button to turn temporary. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 61 ++++++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 60666810..260caf26 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -38,16 +38,19 @@ public class ModPanelSettingsTab( public void DrawContent() { - using var child = ImUtf8.Child("##settings"u8, default); - if (!child) + using var table = ImUtf8.Table("##settings"u8, 1, ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail()); + if (!table) return; _inherited = selection.Collection != collectionManager.Active.Current; _temporary = selection.TemporarySettings != null; _locked = (selection.TemporarySettings?.Lock ?? 0) > 0; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableNextColumn(); DrawTemporaryWarning(); DrawInheritedWarning(); - UiHelpers.DefaultLineSpace(); + ImGui.Dummy(Vector2.Zero); communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); DrawEnabledInput(); tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); @@ -56,6 +59,7 @@ public class ModPanelSettingsTab( tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); + ImGui.TableNextColumn(); communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); @@ -71,7 +75,8 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiCol.Button.Tinted(ColorId.TemporaryModSettingsTint)); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImUtf8.ButtonEx($"These settings are temporary from {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", width, + if (ImUtf8.ButtonEx($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", + width, _locked)) collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); @@ -174,23 +179,45 @@ public class ModPanelSettingsTab( /// private void DrawRemoveSettings() { - if (_inherited || selection.Settings == ModSettings.Empty) + var drawInherited = !_inherited && selection.Settings != ModSettings.Empty; + if (!drawInherited && _temporary) return; - var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine(ImGui.GetWindowWidth() - ImUtf8.CalcTextSize("Inherit Settings"u8).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); - if (!ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 - + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) - return; + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X: 0; + var offset = (drawInherited, _temporary) switch + { + (true, true) => ImUtf8.CalcTextSize("Inherit Settings"u8).X + ImGui.GetStyle().FramePadding.X * 2, + (false, false) => ImUtf8.CalcTextSize("Turn Temporary"u8).X + ImGui.GetStyle().FramePadding.X * 2, + (true, false) => ImUtf8.CalcTextSize("Inherit Settings"u8).X + + ImUtf8.CalcTextSize("Turn Temporary"u8).X + + ImGui.GetStyle().FramePadding.X * 4 + + ImGui.GetStyle().ItemSpacing.X, + (false, true) => 0, // can not happen + }; - if (_temporary) + ImGui.SameLine(ImGui.GetWindowWidth() - offset - scroll); + if (!_temporary + && ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + new TemporaryModSettings(selection.Settings, "yourself")); + if (drawInherited) { - selection.TemporarySettings!.ForceInherit = true; - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); - } - else - { - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + if (!_temporary) + ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); + if (ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = true; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + } + } } } } From 6374362b2871a5fefe68a13a04a8da2de9171869 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 31 Dec 2024 17:05:32 +0000 Subject: [PATCH 513/865] [CI] Updating repo.json for testing_1.3.2.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 97e55af0..1b952d94 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.2.0", - "TestingAssemblyVersion": "1.3.2.1", + "TestingAssemblyVersion": "1.3.2.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0eed5f1707a025e376452d6b9b83f2b74d61db9a Mon Sep 17 00:00:00 2001 From: "N. Lo." Date: Sat, 21 Dec 2024 01:16:57 +0100 Subject: [PATCH 514/865] Add a watched plugin to Support Info --- Penumbra/Penumbra.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 69dfe3e8..33ce9f40 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -187,7 +187,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) From af7a8fbddd2b9285e176a03bc720733dddbb436b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:10:37 +0100 Subject: [PATCH 515/865] Fix bug with atch counter. --- Penumbra/Collections/CollectionCounters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/CollectionCounters.cs b/Penumbra/Collections/CollectionCounters.cs index 91d240d6..6ca0d0a0 100644 --- a/Penumbra/Collections/CollectionCounters.cs +++ b/Penumbra/Collections/CollectionCounters.cs @@ -24,5 +24,5 @@ public struct CollectionCounters(int changeCounter) /// Increment the number of ATCH-relevant changes in the effective file list. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IncrementAtch() - => ++Imc; + => ++Atch; } From 9a457a1a953b8bf6bdce428ce62eb0bd5d027582 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:11:05 +0100 Subject: [PATCH 516/865] Add debug panel to check changed item identification for paths. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index c5168109..8b2bcd77 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -36,6 +36,7 @@ using static OtterGui.Raii.ImRaii; using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; +using Penumbra.GameData.Data; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; @@ -102,6 +103,7 @@ public class DebugTab : Window, ITab, IUiService private readonly HookOverrideDrawer _hookOverrides; private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; + private readonly ObjectIdentification _objectIdentification; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -112,7 +114,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -151,6 +153,7 @@ public class DebugTab : Window, ITab, IUiService _rsfService = rsfService; _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; + _objectIdentification = objectIdentification; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -734,8 +737,37 @@ public class DebugTab : Window, ITab, IUiService DrawActionTmbs(); DrawStainTemplates(); DrawAtch(); + DrawChangedItemTest(); } + private string _changedItemPath = string.Empty; + private readonly Dictionary _changedItems = []; + + private void DrawChangedItemTest() + { + using var node = TreeNode("Changed Item Test"); + if (!node) + return; + + if (ImUtf8.InputText("##ChangedItemTest"u8, ref _changedItemPath, "Changed Item File Path..."u8)) + { + _changedItems.Clear(); + _objectIdentification.Identify(_changedItems, _changedItemPath); + } + + if (_changedItems.Count == 0) + return; + + using var list = ImUtf8.ListBox("##ChangedItemList"u8, + new Vector2(ImGui.GetContentRegionAvail().X, 8 * ImGui.GetTextLineHeightWithSpacing())); + if (!list) + return; + + foreach (var item in _changedItems) + ImUtf8.Selectable(item.Key); + } + + private string _emoteSearchFile = string.Empty; private string _emoteSearchName = string.Empty; From 756537c7760448176a254a8e679e7dbad0afebb1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:11:26 +0100 Subject: [PATCH 517/865] Add Turn Permanent button for temporary settings and improve buttons, make secure. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 82 +++++++++++++++------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 260caf26..417c7be2 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,4 +1,5 @@ using ImGuiNET; +using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -19,7 +20,8 @@ public class ModPanelSettingsTab( ModSelection selection, TutorialService tutorial, CommunicatorService communicator, - ModGroupDrawer modGroupDrawer) + ModGroupDrawer modGroupDrawer, + Configuration config) : ITab, IUiService { private bool _inherited; @@ -180,32 +182,28 @@ public class ModPanelSettingsTab( private void DrawRemoveSettings() { var drawInherited = !_inherited && selection.Settings != ModSettings.Empty; - if (!drawInherited && _temporary) - return; - - var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X: 0; - var offset = (drawInherited, _temporary) switch - { - (true, true) => ImUtf8.CalcTextSize("Inherit Settings"u8).X + ImGui.GetStyle().FramePadding.X * 2, - (false, false) => ImUtf8.CalcTextSize("Turn Temporary"u8).X + ImGui.GetStyle().FramePadding.X * 2, - (true, false) => ImUtf8.CalcTextSize("Inherit Settings"u8).X - + ImUtf8.CalcTextSize("Turn Temporary"u8).X - + ImGui.GetStyle().FramePadding.X * 4 - + ImGui.GetStyle().ItemSpacing.X, - (false, true) => 0, // can not happen - }; - + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X : 0; + var buttonSize = ImUtf8.CalcTextSize("Turn Permanent_"u8).X; + var offset = drawInherited + ? buttonSize + ImUtf8.CalcTextSize("Inherit Settings"u8).X + ImGui.GetStyle().FramePadding.X * 4 + ImGui.GetStyle().ItemSpacing.X + : buttonSize + ImGui.GetStyle().FramePadding.X * 2; ImGui.SameLine(ImGui.GetWindowWidth() - offset - scroll); - if (!_temporary - && ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - new TemporaryModSettings(selection.Settings, "yourself")); + var enabled = config.DeleteModModifier.IsActive(); if (drawInherited) { - if (!_temporary) - ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X); - if (ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 - + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + var inherit = (enabled, _locked) switch + { + (true, false) => ImUtf8.ButtonEx("Inherit Settings"u8, + "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, false), + (false, false) => ImUtf8.ButtonEx("Inherit Settings"u8, + $"Remove current settings from this collection so that it can inherit them.\nHold {config.DeleteModModifier} to inherit.", + default, true), + (_, true) => ImUtf8.ButtonEx("Inherit Settings"u8, + "Remove current settings from this collection so that it can inherit them.\nThe settings are currently locked and can not be changed."u8, + default, true), + }; + if (inherit) { if (_temporary) { @@ -218,6 +216,42 @@ public class ModPanelSettingsTab( collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); } } + + ImGui.SameLine(); + } + + if (_temporary) + { + var overwrite = enabled + ? ImUtf8.ButtonEx("Turn Permanent"u8, + "Overwrite the actual settings for this mod in this collection with the current temporary settings."u8, + new Vector2(buttonSize, 0)) + : ImUtf8.ButtonEx("Turn Permanent"u8, + $"Overwrite the actual settings for this mod in this collection with the current temporary settings.\nHold {config.DeleteModModifier} to overwrite.", + new Vector2(buttonSize, 0), true); + if (overwrite) + { + var settings = collectionManager.Active.Current.GetTempSettings(selection.Mod!.Index)!; + if (settings.ForceInherit) + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod, true); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod, settings.Enabled); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod, settings.Priority); + foreach (var (setting, index) in settings.Settings.WithIndex()) + collectionManager.Editor.SetModSetting(collectionManager.Active.Current, selection.Mod, index, setting); + } + + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod, null); + } + } + else + { + if (ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + new TemporaryModSettings(selection.Settings, "yourself")); } } } From 349241d0ab9cf8bd9cfb19031dfe0b12202c42a9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 7 Jan 2025 16:49:19 +0100 Subject: [PATCH 518/865] Better attribution of authors in item swap. --- Penumbra/Mods/ModCreator.cs | 4 +-- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 31 ++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 1af9c1db..bdc16b72 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -30,12 +30,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 = "") + public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null) { try { var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true); - dataEditor.CreateMeta(newDir, newName, Config.DefaultModAuthor, description, "1.0", string.Empty); + dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty); CreateDefaultFiles(newDir); return newDir; } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 8f1ed8d6..e590eb1e 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -12,6 +12,7 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Mods.Groups; @@ -275,16 +276,38 @@ public class ItemSwapTab : IDisposable, ITab, IUiService case SwapType.Hair: case SwapType.Tail: return - $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}."; + $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}"; case SwapType.BetweenSlots: return - $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}."; + $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; default: return - $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}."; + $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; } } + private string OriginalAuthor() + { + if (_mod!.Author.IsEmpty || _mod!.Author.Text is "TexTools User" or DefaultTexToolsData.Author) + return "."; + + return $" by {_mod!.Author}."; + } + + private string CreateAuthor() + { + if (_mod!.Author.IsEmpty) + return _config.DefaultModAuthor; + if (_mod!.Author.Text == _config.DefaultModAuthor) + return _config.DefaultModAuthor; + if (_mod!.Author.Text is "TexTools User" or DefaultTexToolsData.Author) + return _config.DefaultModAuthor; + if (_config.DefaultModAuthor is DefaultTexToolsData.Author) + return _mod!.Author; + + return $"{_mod!.Author} (Swap by {_config.DefaultModAuthor})"; + } + private void UpdateOption() { _selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName); @@ -296,7 +319,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private void CreateMod() { - var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName, CreateDescription()); + var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName, CreateDescription(), CreateAuthor()); if (newDir == null) return; From f07780cf7babb92e7421bea0df59ad8d6dc00592 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 8 Jan 2025 20:02:14 +0100 Subject: [PATCH 519/865] Add RenderTargetHdrEnabler --- Penumbra.GameData | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../PostProcessing/RenderTargetHdrEnabler.cs | 136 ++++++++++++++++++ .../PostProcessing/ShaderReplacementFixer.cs | 9 ++ Penumbra/Penumbra.json | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 54 ++++++- Penumbra/UI/Tabs/SettingsTab.cs | 17 +++ 8 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index 33de79bc..d5f92966 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 33de79bc62eb014298856ed5c6b6edbe819db26c +Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ec5784f8..df44a51a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -110,6 +110,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool KeepDefaultMetaChanges { get; set; } = false; public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; public bool EditRawTileTransforms { get; set; } = false; + public bool HdrRenderTargets { get; set; } = true; public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 2aeeb14b..b95e5789 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -86,6 +86,7 @@ public class HookOverrides public bool ModelRendererOnRenderMaterial; public bool ModelRendererUnkFunc; public bool PrepareColorTable; + public bool RenderTargetManagerInitialize; } public struct ResourceLoadingHooks diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs new file mode 100644 index 00000000..d620935e --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -0,0 +1,136 @@ +using System.Collections.Immutable; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +public unsafe class RenderTargetHdrEnabler : IService, IDisposable +{ + /// This array must be sorted by CreationOrder ascending. + private static readonly ImmutableArray ForcedTextureConfigs = + [ + new(9, TextureFormat.R16G16B16A16_FLOAT, "Main Diffuse GBuffer"), + new(10, TextureFormat.R16G16B16A16_FLOAT, "Hair Diffuse GBuffer"), + ]; + + private static readonly IComparer ForcedTextureConfigComparer + = Comparer.Create((lhs, rhs) => lhs.CreationOrder.CompareTo(rhs.CreationOrder)); + + private readonly Configuration _config; + + private readonly ThreadLocal _textureIndices = new(() => new(-1, -1)); + private readonly ThreadLocal?> _textures = new(() => null); + + public TextureReportRecord[]? TextureReport { get; private set; } + + [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] + private Hook _renderTargetManagerInitialize = null!; + + [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] + private Hook _createTexture2D = null!; + + public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) + { + _config = config; + interop.InitializeFromAttributes(this); + if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) + _renderTargetManagerInitialize.Enable(); + } + + ~RenderTargetHdrEnabler() + => Dispose(false); + + public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) + { + var i = ForcedTextureConfigs.BinarySearch(new(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); + return i >= 0 ? ForcedTextureConfigs[i] : null; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool _) + { + _renderTargetManagerInitialize.Disable(); + if (_createTexture2D.IsEnabled) + _createTexture2D.Disable(); + + _createTexture2D.Dispose(); + _renderTargetManagerInitialize.Dispose(); + } + + private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) + { + _createTexture2D.Enable(); + _textureIndices.Value = new(0, 0); + _textures.Value = _config.DebugMode ? [] : null; + try + { + return _renderTargetManagerInitialize.Original(@this); + } + finally + { + if (_textures.Value != null) + { + TextureReport = CreateTextureReport(@this, _textures.Value); + _textures.Value = null; + } + _textureIndices.Value = new(-1, -1); + _createTexture2D.Disable(); + } + } + + private Texture* CreateTexture2DDetour( + Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) + { + var originalTextureFormat = textureFormat; + var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new(-1, -1); + if (indices.ConfigIndex >= 0 && indices.ConfigIndex < ForcedTextureConfigs.Length && + ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) + { + var config = ForcedTextureConfigs[indices.ConfigIndex++]; + textureFormat = (uint)config.ForcedTextureFormat; + } + + if (indices.CreationOrder >= 0) + { + ++indices.CreationOrder; + _textureIndices.Value = indices; + } + + var texture = _createTexture2D.Original(@this, size, mipLevel, textureFormat, flags, unk); + if (_textures.IsValueCreated) + _textures.Value?.Add((nint)texture, (indices.CreationOrder - 1, originalTextureFormat)); + return texture; + } + + private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, Dictionary textures) + { + var rtmTextures = new Span(renderTargetManager, sizeof(RenderTargetManager) / sizeof(nint)); + var report = new List(); + for (var i = 0; i < rtmTextures.Length; ++i) + { + if (textures.TryGetValue(rtmTextures[i], out var texture)) + report.Add(new(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); + } + return report.ToArray(); + } + + private delegate nint RenderTargetManagerInitializeFunc(RenderTargetManager* @this); + + private delegate Texture* CreateTexture2DFunc(Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk); + + private record struct TextureIndices(int CreationOrder, int ConfigIndex); + + public readonly record struct ForcedTextureConfig(int CreationOrder, TextureFormat ForcedTextureFormat, string Comment); + + public readonly record struct TextureReportRecord(nint Offset, int CreationOrder, TextureFormat OriginalTextureFormat); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 40958eb4..3b41e752 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -7,6 +7,7 @@ using OtterGui.Classes; using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData; +using Penumbra.GameData.Files.MaterialStructs; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; using Penumbra.Services; @@ -462,8 +463,16 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return mtrlResource; } + private static int GetDataSetExpectedSize(uint dataFlags) + => (dataFlags & 4) != 0 + ? ColorTable.Size + ((dataFlags & 8) != 0 ? ColorDyeTable.Size : 0) + : 0; + private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { + if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) + Penumbra.Log.Warning($"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); + // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForColorTable() == 0) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 4790da18..968bb750 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -10,7 +10,7 @@ "Tags": [ "modding" ], "DalamudApiLevel": 11, "LoadPriority": 69420, - "LoadState": 2, + "LoadRequiredState": 2, "LoadSync": true, "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 8b2bcd77..a759e11a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,6 +42,7 @@ using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; +using CSGraphics = FFXIVClientStructs.FFXIV.Client.Graphics; namespace Penumbra.UI.Tabs.Debug; @@ -104,6 +105,7 @@ public class DebugTab : Window, ITab, IUiService private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; + private readonly RenderTargetHdrEnabler _renderTargetHdrEnabler; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -114,7 +116,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetHdrEnabler renderTargetHdrEnabler) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -154,6 +156,7 @@ public class DebugTab : Window, ITab, IUiService _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; _objectIdentification = objectIdentification; + _renderTargetHdrEnabler = renderTargetHdrEnabler; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -189,6 +192,7 @@ public class DebugTab : Window, ITab, IUiService DrawData(); DrawCrcCache(); DrawResourceProblems(); + DrawRenderTargets(); _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); @@ -1135,6 +1139,54 @@ public class DebugTab : Window, ITab, IUiService } + /// Draw information about render targets. + private unsafe void DrawRenderTargets() + { + if (!ImGui.CollapsingHeader("Render Targets")) + return; + + var report = _renderTargetHdrEnabler.TextureReport; + if (report == null) + { + ImGui.TextUnformatted("The RenderTargetManager report has not been gathered."); + ImGui.TextUnformatted("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."); + return; + } + + using var table = Table("##RenderTargetTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var record in report) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"0x{record.Offset:X}"); + ImGui.TableNextColumn(); + ImUtf8.Text($"{record.CreationOrder}"); + ImGui.TableNextColumn(); + ImUtf8.Text($"{record.OriginalTextureFormat}"); + ImGui.TableNextColumn(); + var texture = *(CSGraphics.Kernel.Texture**)((nint)CSGraphics.Render.RenderTargetManager.Instance() + record.Offset); + if (texture != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), texture->TextureFormat != record.OriginalTextureFormat); + ImUtf8.Text($"{texture->TextureFormat}"); + } + ImGui.TableNextColumn(); + var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); + if (forcedConfig.HasValue) + ImGui.TextUnformatted(forcedConfig.Value.Comment); + } + } + + /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 46e214cf..64fa57a5 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -773,6 +773,7 @@ public class SettingsTab : ITab, IUiService DrawCrashHandler(); DrawMinimumDimensionConfig(); + DrawHdrRenderTargets(); Checkbox("Auto Deduplicate on Import", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); @@ -902,6 +903,22 @@ public class SettingsTab : ITab, IUiService _config.Save(); } + private void DrawHdrRenderTargets() + { + var item = _config.HdrRenderTargets ? 1 : 0; + ImGui.SetNextItemWidth(ImGui.CalcTextSize("M").X * 5.0f + ImGui.GetFrameHeight()); + var edited = ImGui.Combo("##hdrRenderTarget", ref item, "SDR\0HDR\0"); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Diffuse Dynamic Range", + "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\nChanging this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."); + + if (!edited) + return; + + _config.HdrRenderTargets = item != 0; + _config.Save(); + } + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. private void DrawEnableHttpApiBox() { From e8300fc5c83acc6f86dbaa5086869f721478317b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 9 Jan 2025 20:42:48 +0100 Subject: [PATCH 520/865] Improve RT-HDR texture comments --- .../Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index d620935e..80106fc9 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -14,8 +14,8 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable /// This array must be sorted by CreationOrder ascending. private static readonly ImmutableArray ForcedTextureConfigs = [ - new(9, TextureFormat.R16G16B16A16_FLOAT, "Main Diffuse GBuffer"), - new(10, TextureFormat.R16G16B16A16_FLOAT, "Hair Diffuse GBuffer"), + new(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), + new(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), ]; private static readonly IComparer ForcedTextureConfigComparer From b83564bce8424daaa7b0facfb91a19dac6062850 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jan 2025 19:55:33 +0100 Subject: [PATCH 521/865] 1.3.3.0 --- Penumbra/UI/Changelog.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index c78ca290..f83c8989 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -56,10 +56,28 @@ public class PenumbraChangelog : IUiService Add1_3_0_0(Changelog); Add1_3_1_0(Changelog); Add1_3_2_0(Changelog); + Add1_3_3_0(Changelog); } #region Changelogs + private static void Add1_3_3_0(Changelog log) + => log.NextVersion("Version 1.3.3.0") + .RegisterHighlight("Added Temporary Settings to collections.") + .RegisterEntry("Settings can be manually turned temporary (and turned back) while editing mod settings via right-click context on the mod or buttons in the settings panel.", 1) + .RegisterEntry("This can be used to test mods or changes without saving those changes permanently or having to reinstate the old settings afterwards.", 1) + .RegisterEntry("More importantly, this can be set via IPC by other plugins, allowing Glamourer to only set and reset temporary settings when applying Mod Associations.", 1) + .RegisterEntry("As an extreme example, it would be possible to only enable the consistent mods for your character in the collection, and let Glamourer handle all outfit mods itself via temporary settings only.", 1) + .RegisterEntry("This required some pretty big changes that were in testing for a while now, but nobody talked about it much so it may still have some bugs or usability issues. Let me know!", 1) + .RegisterHighlight("Added an option to automatically select the collection assigned to the current character on login events. This is off by default.") + .RegisterEntry("Added partial copying of color tables in material editing via right-click context menu entries on the import buttons.") + .RegisterHighlight("Added handling for TMB files cached by the game that should resolve issues of leaky TMBs from animation and VFX mods.") + .RegisterEntry("The enabled checkbox, Priority and Inheriting buttons now stick at the top of the Mod Settings panel even when scrolling down for specific settings.") + .RegisterEntry("When creating new mods with Item Swap, the attributed author of the resulting mod was improved.") + .RegisterEntry("Fixed an issue with rings in the On-Screen tab and in the data sent over to other plugins via IPC.") + .RegisterEntry("Fixed some issues when writing material files that resulted in technically valid files that still caused some issues with the game for unknown reasons.") + .RegisterEntry("Fixed some ImGui assertions."); + private static void Add1_3_2_0(Changelog log) => log.NextVersion("Version 1.3.2.0") .RegisterHighlight("Added ATCH meta manipulations that allow the composite editing of attachment points across multiple mods.") From e6872cff64a8764d21bc7d37a2e11083825b3596 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 10 Jan 2025 19:00:27 +0000 Subject: [PATCH 522/865] [CI] Updating repo.json for 1.3.3.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1b952d94..25dd6da4 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.3.2.0", - "TestingAssemblyVersion": "1.3.2.2", + "AssemblyVersion": "1.3.3.0", + "TestingAssemblyVersion": "1.3.3.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.2.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.2.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From d4e6688369961f38b365ee4611dc7d1a807ef7b4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 13:26:43 +0100 Subject: [PATCH 523/865] Fix issue when empty settings are turned temporary. --- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 417c7be2..2420f06b 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -249,9 +249,10 @@ public class ModPanelSettingsTab( } else { + var actual = collectionManager.Active.Current.GetActualSettings(selection.Mod!.Index).Settings; if (ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - new TemporaryModSettings(selection.Settings, "yourself")); + new TemporaryModSettings(actual, "yourself")); } } } From 0758739666917a98304d8cdb8288924a67152084 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 13:46:08 +0100 Subject: [PATCH 524/865] Cleanup UI code. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 57 ++----------------- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 59 ++++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 34 +++++++---- 3 files changed, 86 insertions(+), 64 deletions(-) create mode 100644 Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index a759e11a..77eeb3d7 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -42,7 +42,6 @@ using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; -using CSGraphics = FFXIVClientStructs.FFXIV.Client.Graphics; namespace Penumbra.UI.Tabs.Debug; @@ -105,7 +104,7 @@ public class DebugTab : Window, ITab, IUiService private readonly RsfService _rsfService; private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; - private readonly RenderTargetHdrEnabler _renderTargetHdrEnabler; + private readonly RenderTargetDrawer _renderTargetDrawer; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -116,7 +115,7 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetHdrEnabler renderTargetHdrEnabler) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -156,7 +155,7 @@ public class DebugTab : Window, ITab, IUiService _globalVariablesDrawer = globalVariablesDrawer; _schedulerService = schedulerService; _objectIdentification = objectIdentification; - _renderTargetHdrEnabler = renderTargetHdrEnabler; + _renderTargetDrawer = renderTargetDrawer; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -192,7 +191,7 @@ public class DebugTab : Window, ITab, IUiService DrawData(); DrawCrcCache(); DrawResourceProblems(); - DrawRenderTargets(); + _renderTargetDrawer.Draw(); _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); @@ -1139,54 +1138,6 @@ public class DebugTab : Window, ITab, IUiService } - /// Draw information about render targets. - private unsafe void DrawRenderTargets() - { - if (!ImGui.CollapsingHeader("Render Targets")) - return; - - var report = _renderTargetHdrEnabler.TextureReport; - if (report == null) - { - ImGui.TextUnformatted("The RenderTargetManager report has not been gathered."); - ImGui.TextUnformatted("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."); - return; - } - - using var table = Table("##RenderTargetTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); - ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); - ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); - ImGui.TableHeadersRow(); - - foreach (var record in report) - { - ImGui.TableNextColumn(); - ImUtf8.Text($"0x{record.Offset:X}"); - ImGui.TableNextColumn(); - ImUtf8.Text($"{record.CreationOrder}"); - ImGui.TableNextColumn(); - ImUtf8.Text($"{record.OriginalTextureFormat}"); - ImGui.TableNextColumn(); - var texture = *(CSGraphics.Kernel.Texture**)((nint)CSGraphics.Render.RenderTargetManager.Instance() + record.Offset); - if (texture != null) - { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), texture->TextureFormat != record.OriginalTextureFormat); - ImUtf8.Text($"{texture->TextureFormat}"); - } - ImGui.TableNextColumn(); - var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); - if (forcedConfig.HasValue) - ImGui.TextUnformatted(forcedConfig.Value.Comment); - } - } - - /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs new file mode 100644 index 00000000..09c8b06c --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -0,0 +1,59 @@ +using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; +using ImGuiNET; +using OtterGui; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Interop.Hooks.PostProcessing; + +namespace Penumbra.UI.Tabs.Debug; + +public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler) : IUiService +{ + /// Draw information about render targets. + public unsafe void Draw() + { + if (!ImUtf8.CollapsingHeader("Render Targets"u8)) + return; + + var report = renderTargetHdrEnabler.TextureReport; + if (report == null) + { + ImUtf8.Text("The RenderTargetManager report has not been gathered."u8); + ImUtf8.Text("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."u8); + return; + } + + using var table = ImUtf8.Table("##RenderTargetTable"u8, 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImUtf8.TableSetupColumn("Offset"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Creation Order"u8, ImGuiTableColumnFlags.WidthStretch, 0.15f); + ImUtf8.TableSetupColumn("Original Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Current Texture Format"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Comment"u8, ImGuiTableColumnFlags.WidthStretch, 0.3f); + ImGui.TableHeadersRow(); + + foreach (var record in report) + { + ImUtf8.DrawTableColumn($"0x{record.Offset:X}"); + ImUtf8.DrawTableColumn($"{record.CreationOrder}"); + ImUtf8.DrawTableColumn($"{record.OriginalTextureFormat}"); + ImGui.TableNextColumn(); + var texture = *(Texture**)((nint)RenderTargetManager.Instance() + + record.Offset); + if (texture != null) + { + using var color = Dalamud.Interface.Utility.Raii.ImRaii.PushColor(ImGuiCol.Text, ImGuiUtil.HalfBlendText(0xFF), + texture->TextureFormat != record.OriginalTextureFormat); + ImUtf8.Text($"{texture->TextureFormat}"); + } + + ImGui.TableNextColumn(); + var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); + if (forcedConfig.HasValue) + ImUtf8.Text(forcedConfig.Value.Comment); + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 64fa57a5..c7f66859 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -10,6 +10,7 @@ using OtterGui.Compression; using OtterGui.Custom; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; @@ -905,18 +906,29 @@ public class SettingsTab : ITab, IUiService private void DrawHdrRenderTargets() { - var item = _config.HdrRenderTargets ? 1 : 0; - ImGui.SetNextItemWidth(ImGui.CalcTextSize("M").X * 5.0f + ImGui.GetFrameHeight()); - var edited = ImGui.Combo("##hdrRenderTarget", ref item, "SDR\0HDR\0"); + ImGui.SetNextItemWidth(ImUtf8.CalcTextSize("M"u8).X * 5.0f + ImGui.GetFrameHeight()); + using (var combo = ImUtf8.Combo("##hdrRenderTarget"u8, _config.HdrRenderTargets ? "HDR"u8 : "SDR"u8)) + { + if (combo) + { + if (ImUtf8.Selectable("HDR"u8, _config.HdrRenderTargets) && !_config.HdrRenderTargets) + { + _config.HdrRenderTargets = true; + _config.Save(); + } + + if (ImUtf8.Selectable("SDR"u8, !_config.HdrRenderTargets) && _config.HdrRenderTargets) + { + _config.HdrRenderTargets = false; + _config.Save(); + } + } + } + ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Diffuse Dynamic Range", - "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\nChanging this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."); - - if (!edited) - return; - - _config.HdrRenderTargets = item != 0; - _config.Save(); + ImUtf8.LabeledHelpMarker("Diffuse Dynamic Range"u8, + "Set the dynamic range that can be used for diffuse colors in materials without causing visual artifacts.\n"u8 + + "Changing this setting requires a game restart. It also only works if Wait for Plugins on Startup is enabled."u8); } /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. From e73b3e85bdd9e136761108299040433a3314e937 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 13:46:28 +0100 Subject: [PATCH 525/865] Autoformat and remove nagging. --- .../PostProcessing/RenderTargetHdrEnabler.cs | 41 +++++++++-------- .../PostProcessing/ShaderReplacementFixer.cs | 46 +++++++++---------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index 80106fc9..b7ae771b 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -14,8 +14,8 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable /// This array must be sorted by CreationOrder ascending. private static readonly ImmutableArray ForcedTextureConfigs = [ - new(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), - new(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), + new ForcedTextureConfig(9, TextureFormat.R16G16B16A16_FLOAT, "Opaque Diffuse GBuffer"), + new ForcedTextureConfig(10, TextureFormat.R16G16B16A16_FLOAT, "Semitransparent Diffuse GBuffer"), ]; private static readonly IComparer ForcedTextureConfigComparer @@ -23,16 +23,17 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private readonly Configuration _config; - private readonly ThreadLocal _textureIndices = new(() => new(-1, -1)); + private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); + private readonly ThreadLocal?> _textures = new(() => null); public TextureReportRecord[]? TextureReport { get; private set; } [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] - private Hook _renderTargetManagerInitialize = null!; + private readonly Hook _renderTargetManagerInitialize = null!; [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] - private Hook _createTexture2D = null!; + private readonly Hook _createTexture2D = null!; public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) { @@ -47,7 +48,7 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) { - var i = ForcedTextureConfigs.BinarySearch(new(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); + var i = ForcedTextureConfigs.BinarySearch(new ForcedTextureConfig(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); return i >= 0 ? ForcedTextureConfigs[i] : null; } @@ -59,10 +60,6 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private void Dispose(bool _) { - _renderTargetManagerInitialize.Disable(); - if (_createTexture2D.IsEnabled) - _createTexture2D.Disable(); - _createTexture2D.Dispose(); _renderTargetManagerInitialize.Dispose(); } @@ -70,8 +67,8 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) { _createTexture2D.Enable(); - _textureIndices.Value = new(0, 0); - _textures.Value = _config.DebugMode ? [] : null; + _textureIndices.Value = new TextureIndices(0, 0); + _textures.Value = _config.DebugMode ? [] : null; try { return _renderTargetManagerInitialize.Original(@this); @@ -80,10 +77,11 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable { if (_textures.Value != null) { - TextureReport = CreateTextureReport(@this, _textures.Value); + TextureReport = CreateTextureReport(@this, _textures.Value); _textures.Value = null; } - _textureIndices.Value = new(-1, -1); + + _textureIndices.Value = new TextureIndices(-1, -1); _createTexture2D.Disable(); } } @@ -92,9 +90,10 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable Device* @this, int* size, byte mipLevel, uint textureFormat, uint flags, uint unk) { var originalTextureFormat = textureFormat; - var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new(-1, -1); - if (indices.ConfigIndex >= 0 && indices.ConfigIndex < ForcedTextureConfigs.Length && - ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) + var indices = _textureIndices.IsValueCreated ? _textureIndices.Value : new TextureIndices(-1, -1); + if (indices.ConfigIndex >= 0 + && indices.ConfigIndex < ForcedTextureConfigs.Length + && ForcedTextureConfigs[indices.ConfigIndex].CreationOrder == indices.CreationOrder) { var config = ForcedTextureConfigs[indices.ConfigIndex++]; textureFormat = (uint)config.ForcedTextureFormat; @@ -112,15 +111,17 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable return texture; } - private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, Dictionary textures) + private static TextureReportRecord[] CreateTextureReport(RenderTargetManager* renderTargetManager, + Dictionary textures) { var rtmTextures = new Span(renderTargetManager, sizeof(RenderTargetManager) / sizeof(nint)); - var report = new List(); + var report = new List(); for (var i = 0; i < rtmTextures.Length; ++i) { if (textures.TryGetValue(rtmTextures[i], out var texture)) - report.Add(new(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); + report.Add(new TextureReportRecord(i * sizeof(nint), texture.TextureIndex, (TextureFormat)texture.TextureFormat)); } + return report.ToArray(); } diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 3b41e752..f70ea06e 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -64,8 +64,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly ResourceHandleDestructor _resourceHandleDestructor; private readonly CommunicatorService _communicator; - private readonly CharacterUtility _utility; - private readonly ModelRenderer _modelRenderer; private readonly HumanSetupScalingHook _humanSetupScalingHook; private readonly ModdedShaderPackageState _skinState; @@ -111,31 +109,31 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - _utility = utility; - _modelRenderer = modelRenderer; - _communicator = communicator; - _humanSetupScalingHook = humanSetupScalingHook; + var utility1 = utility; + var modelRenderer1 = modelRenderer; + _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->SkinShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultSkinShpkResource); + () => (ShaderPackageResourceHandle**)&utility1.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultSkinShpkResource); _characterStockingsState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterStockingsShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterStockingsShpkResource); + () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterStockingsShpkResource); _characterLegacyState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&_utility.Address->CharacterLegacyShpkResource, - () => (ShaderPackageResourceHandle*)_utility.DefaultCharacterLegacyShpkResource); - _irisState = new ModdedShaderPackageState(() => _modelRenderer.IrisShaderPackage, () => _modelRenderer.DefaultIrisShaderPackage); - _characterGlassState = new ModdedShaderPackageState(() => _modelRenderer.CharacterGlassShaderPackage, - () => _modelRenderer.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTransparencyShaderPackage, - () => _modelRenderer.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new ModdedShaderPackageState(() => _modelRenderer.CharacterTattooShaderPackage, - () => _modelRenderer.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new ModdedShaderPackageState(() => _modelRenderer.CharacterOcclusionShaderPackage, - () => _modelRenderer.DefaultCharacterOcclusionShaderPackage); + () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterLegacyShpkResource); + _irisState = new ModdedShaderPackageState(() => modelRenderer1.IrisShaderPackage, () => modelRenderer1.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => modelRenderer1.CharacterGlassShaderPackage, + () => modelRenderer1.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTransparencyShaderPackage, + () => modelRenderer1.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTattooShaderPackage, + () => modelRenderer1.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer1.CharacterOcclusionShaderPackage, + () => modelRenderer1.DefaultCharacterOcclusionShaderPackage); _hairMaskState = - new ModdedShaderPackageState(() => _modelRenderer.HairMaskShaderPackage, () => _modelRenderer.DefaultHairMaskShaderPackage); + new ModdedShaderPackageState(() => modelRenderer1.HairMaskShaderPackage, () => modelRenderer1.DefaultHairMaskShaderPackage); _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], @@ -463,6 +461,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic return mtrlResource; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static int GetDataSetExpectedSize(uint dataFlags) => (dataFlags & 4) != 0 ? ColorTable.Size + ((dataFlags & 8) != 0 ? ColorDyeTable.Size : 0) @@ -471,7 +470,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) - Penumbra.Log.Warning($"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); + Penumbra.Log.Warning( + $"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); // If we don't have any on-screen instances of modded characterlegacy.shpk, we don't need the slow path at all. if (!Enabled || GetTotalMaterialCountForColorTable() == 0) From c99a7884bb38d8ce2fd1d21c4b705e51b946369e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Jan 2025 12:49:05 +0000 Subject: [PATCH 526/865] [CI] Updating repo.json for 1.3.3.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 25dd6da4..41956dc3 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.3.3.0", - "TestingAssemblyVersion": "1.3.3.0", + "AssemblyVersion": "1.3.3.1", + "TestingAssemblyVersion": "1.3.3.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 7b2e82b27f1f378d82e68055df48ae758af0ad71 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 15:22:04 +0100 Subject: [PATCH 527/865] Add some HDR related debug data and support info. --- .../PostProcessing/RenderTargetHdrEnabler.cs | 36 ++++++++++++++-- .../PostProcessing/ShaderReplacementFixer.cs | 34 ++++++++------- Penumbra/Penumbra.cs | 13 +++--- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 41 ++++++++++++++++++- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index b7ae771b..41c4dab1 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -1,11 +1,13 @@ using System.Collections.Immutable; using Dalamud.Hooking; +using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui.Services; using Penumbra.GameData; +using Penumbra.Services; namespace Penumbra.Interop.Hooks.PostProcessing; @@ -21,9 +23,9 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private static readonly IComparer ForcedTextureConfigComparer = Comparer.Create((lhs, rhs) => lhs.CreationOrder.CompareTo(rhs.CreationOrder)); - private readonly Configuration _config; - - private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); + private readonly Configuration _config; + private readonly Tuple _share; + private readonly ThreadLocal _textureIndices = new(() => new TextureIndices(-1, -1)); private readonly ThreadLocal?> _textures = new(() => null); @@ -35,17 +37,42 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] private readonly Hook _createTexture2D = null!; - public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config) + public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config, IDalamudPluginInterface pi, + DalamudConfigService dalamudConfig) { _config = config; interop.InitializeFromAttributes(this); if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) _renderTargetManagerInitialize.Enable(); + + _share = pi.GetOrCreateData("Penumbra.RenderTargetHDR.V1", () => + { + bool? waitForPlugins = dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s) ? s : null; + return new Tuple(waitForPlugins, config.HdrRenderTargets, + !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize, [0], [false]); + }); + ++_share.Item4[0]; } + public bool? FirstLaunchWaitForPluginsState + => _share.Item1; + + public bool FirstLaunchHdrState + => _share.Item2; + + public bool FirstLaunchHdrHookOverrideState + => _share.Item3; + + public int PenumbraReloadCount + => _share.Item4[0]; + + public bool HdrEnabledSuccess + => _share.Item5[0]; + ~RenderTargetHdrEnabler() => Dispose(false); + public static ForcedTextureConfig? GetForcedTextureConfig(int creationOrder) { var i = ForcedTextureConfigs.BinarySearch(new ForcedTextureConfig(creationOrder, 0, string.Empty), ForcedTextureConfigComparer); @@ -67,6 +94,7 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) { _createTexture2D.Enable(); + _share.Item5[0] = true; _textureIndices.Value = new TextureIndices(0, 0); _textures.Value = _config.DebugMode ? [] : null; try diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index f70ea06e..cae37776 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -109,31 +109,29 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - var utility1 = utility; - var modelRenderer1 = modelRenderer; _communicator = communicator; _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&utility1.Address->SkinShpkResource, - () => (ShaderPackageResourceHandle*)utility1.DefaultSkinShpkResource); + () => (ShaderPackageResourceHandle**)&utility.Address->SkinShpkResource, + () => (ShaderPackageResourceHandle*)utility.DefaultSkinShpkResource); _characterStockingsState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterStockingsShpkResource, - () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterStockingsShpkResource); + () => (ShaderPackageResourceHandle**)&utility.Address->CharacterStockingsShpkResource, + () => (ShaderPackageResourceHandle*)utility.DefaultCharacterStockingsShpkResource); _characterLegacyState = new ModdedShaderPackageState( - () => (ShaderPackageResourceHandle**)&utility1.Address->CharacterLegacyShpkResource, - () => (ShaderPackageResourceHandle*)utility1.DefaultCharacterLegacyShpkResource); - _irisState = new ModdedShaderPackageState(() => modelRenderer1.IrisShaderPackage, () => modelRenderer1.DefaultIrisShaderPackage); - _characterGlassState = new ModdedShaderPackageState(() => modelRenderer1.CharacterGlassShaderPackage, - () => modelRenderer1.DefaultCharacterGlassShaderPackage); - _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTransparencyShaderPackage, - () => modelRenderer1.DefaultCharacterTransparencyShaderPackage); - _characterTattooState = new ModdedShaderPackageState(() => modelRenderer1.CharacterTattooShaderPackage, - () => modelRenderer1.DefaultCharacterTattooShaderPackage); - _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer1.CharacterOcclusionShaderPackage, - () => modelRenderer1.DefaultCharacterOcclusionShaderPackage); + () => (ShaderPackageResourceHandle**)&utility.Address->CharacterLegacyShpkResource, + () => (ShaderPackageResourceHandle*)utility.DefaultCharacterLegacyShpkResource); + _irisState = new ModdedShaderPackageState(() => modelRenderer.IrisShaderPackage, () => modelRenderer.DefaultIrisShaderPackage); + _characterGlassState = new ModdedShaderPackageState(() => modelRenderer.CharacterGlassShaderPackage, + () => modelRenderer.DefaultCharacterGlassShaderPackage); + _characterTransparencyState = new ModdedShaderPackageState(() => modelRenderer.CharacterTransparencyShaderPackage, + () => modelRenderer.DefaultCharacterTransparencyShaderPackage); + _characterTattooState = new ModdedShaderPackageState(() => modelRenderer.CharacterTattooShaderPackage, + () => modelRenderer.DefaultCharacterTattooShaderPackage); + _characterOcclusionState = new ModdedShaderPackageState(() => modelRenderer.CharacterOcclusionShaderPackage, + () => modelRenderer.DefaultCharacterOcclusionShaderPackage); _hairMaskState = - new ModdedShaderPackageState(() => modelRenderer1.HairMaskShaderPackage, () => modelRenderer1.DefaultHairMaskShaderPackage); + new ModdedShaderPackageState(() => modelRenderer.HairMaskShaderPackage, () => modelRenderer.DefaultHairMaskShaderPackage); _humanSetupScalingHook.SetupReplacements += SetupHssReplacements; _humanOnRenderMaterialHook = hooks.CreateHook("Human.OnRenderMaterial", vTables.HumanVTable[64], diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 33ce9f40..b6009627 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,6 +23,7 @@ using Lumina.Excel.Sheets; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; +using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; namespace Penumbra; @@ -205,9 +206,10 @@ public class Penumbra : IDalamudPlugin public string GatherSupportInformation() { - var sb = new StringBuilder(10240); - var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); - 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 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,9 +225,10 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); 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($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n"); - sb.Append( - $"> **`Synchronous Load (Dalamud): `** {(_services.GetService().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.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/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs index 09c8b06c..c8c90e09 100644 --- a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -4,18 +4,57 @@ using ImGuiNET; using OtterGui; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Services; namespace Penumbra.UI.Tabs.Debug; -public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler) : IUiService +public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler, DalamudConfigService dalamudConfig, Configuration config) : IUiService { + private void DrawStatistics() + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Wait For Plugins (Now)"); + ImUtf8.Text("Wait For Plugins (First Launch)"); + + ImUtf8.Text("HDR Enabled (Now)"); + ImUtf8.Text("HDR Enabled (First Launch)"); + + ImUtf8.Text("HDR Hook Overriden (Now)"); + ImUtf8.Text("HDR Hook Overriden (First Launch)"); + + ImUtf8.Text("HDR Detour Called"); + ImUtf8.Text("Penumbra Reload Count"); + } + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{(dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool w) ? w.ToString() : "Unknown")}"); + ImUtf8.Text($"{renderTargetHdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"}"); + + ImUtf8.Text($"{config.HdrRenderTargets}"); + ImUtf8.Text($"{renderTargetHdrEnabler.FirstLaunchHdrState}"); + + ImUtf8.Text($"{HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize}"); + ImUtf8.Text($"{!renderTargetHdrEnabler.FirstLaunchHdrHookOverrideState}"); + + ImUtf8.Text($"{renderTargetHdrEnabler.HdrEnabledSuccess}"); + ImUtf8.Text($"{renderTargetHdrEnabler.PenumbraReloadCount}"); + } + } + /// Draw information about render targets. public unsafe void Draw() { if (!ImUtf8.CollapsingHeader("Render Targets"u8)) return; + DrawStatistics(); + ImUtf8.Dummy(0); + ImGui.Separator(); + ImUtf8.Dummy(0); var report = renderTargetHdrEnabler.TextureReport; if (report == null) { From 2753c786fc938cb63a729945974d1e334e3a3934 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 15:55:14 +0100 Subject: [PATCH 528/865] Only put out warnings if the path is rooted. --- Penumbra.String | 2 +- .../Hooks/PostProcessing/ShaderReplacementFixer.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra.String b/Penumbra.String index 0647fbc5..b9003b97 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0647fbc5017ef9ced3f3ce1c2496eefd57c5b7a8 +Subproject commit b9003b97da2d1191fa203a4d66956bc54c21db2a diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index cae37776..8e12662e 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -11,6 +11,7 @@ using Penumbra.GameData.Files.MaterialStructs; using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Structs; using Penumbra.Services; +using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRenderer; using ModelRenderer = Penumbra.Interop.Services.ModelRenderer; @@ -109,8 +110,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic CommunicatorService communicator, HookManager hooks, CharacterBaseVTables vTables, HumanSetupScalingHook humanSetupScalingHook) { _resourceHandleDestructor = resourceHandleDestructor; - _communicator = communicator; - _humanSetupScalingHook = humanSetupScalingHook; + _communicator = communicator; + _humanSetupScalingHook = humanSetupScalingHook; _skinState = new ModdedShaderPackageState( () => (ShaderPackageResourceHandle**)&utility.Address->SkinShpkResource, @@ -467,7 +468,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private Texture* PrepareColorTableDetour(MaterialResourceHandle* thisPtr, byte stain0Id, byte stain1Id) { - if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags)) + if (thisPtr->DataSetSize < GetDataSetExpectedSize(thisPtr->DataFlags) && Utf8GamePath.IsRooted(thisPtr->FileName.AsSpan())) Penumbra.Log.Warning( $"Material at {thisPtr->FileName} has data set of size {thisPtr->DataSetSize} bytes, but should have at least {GetDataSetExpectedSize(thisPtr->DataFlags)} bytes. This may cause crashes due to access violations."); @@ -507,9 +508,8 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic private readonly ConcurrentSet _materials = new(); // ConcurrentDictionary.Count uses a lock in its current implementation. - private uint _materialCount = 0; - - private ulong _slowPathCallDelta = 0; + private uint _materialCount; + private ulong _slowPathCallDelta; public uint MaterialCount { From 7f52777fd48011606d6317934bcdd02f5183573e Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Jan 2025 14:58:57 +0000 Subject: [PATCH 529/865] [CI] Updating repo.json for testing_1.3.3.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 41956dc3..ec1fc623 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.1", + "TestingAssemblyVersion": "1.3.3.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 6ea38eac0a2b050940a030423012344cc26cbb21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 17:59:44 +0100 Subject: [PATCH 530/865] Share PeSigScanner and use in RenderTargetHdrEnabler because of ReShade. --- .../PostProcessing/RenderTargetHdrEnabler.cs | 33 +++++++++++-------- .../Hooks/ResourceLoading/PapHandler.cs | 4 +-- .../Hooks/ResourceLoading/PapRewriter.cs | 9 ++--- .../Hooks/ResourceLoading/PeSigScanner.cs | 3 +- .../Hooks/ResourceLoading/ResourceLoader.cs | 4 +-- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs index 41c4dab1..653d9c1a 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/RenderTargetHdrEnabler.cs @@ -7,6 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using OtterGui.Services; using Penumbra.GameData; +using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Services; namespace Penumbra.Interop.Hooks.PostProcessing; @@ -31,19 +32,23 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable public TextureReportRecord[]? TextureReport { get; private set; } - [Signature(Sigs.RenderTargetManagerInitialize, DetourName = nameof(RenderTargetManagerInitializeDetour))] - private readonly Hook _renderTargetManagerInitialize = null!; - - [Signature(Sigs.DeviceCreateTexture2D, DetourName = nameof(CreateTexture2DDetour))] - private readonly Hook _createTexture2D = null!; + private readonly Hook? _renderTargetManagerInitialize; + private readonly Hook? _createTexture2D; public RenderTargetHdrEnabler(IGameInteropProvider interop, Configuration config, IDalamudPluginInterface pi, - DalamudConfigService dalamudConfig) + DalamudConfigService dalamudConfig, PeSigScanner peScanner) { _config = config; - interop.InitializeFromAttributes(this); - if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) - _renderTargetManagerInitialize.Enable(); + if (peScanner.TryScanText(Sigs.RenderTargetManagerInitialize, out var initializeAddress) + && peScanner.TryScanText(Sigs.DeviceCreateTexture2D, out var createAddress)) + { + _renderTargetManagerInitialize = + interop.HookFromAddress(initializeAddress, RenderTargetManagerInitializeDetour); + _createTexture2D = interop.HookFromAddress(createAddress, CreateTexture2DDetour); + + if (config.HdrRenderTargets && !HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize) + _renderTargetManagerInitialize.Enable(); + } _share = pi.GetOrCreateData("Penumbra.RenderTargetHDR.V1", () => { @@ -87,19 +92,19 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable private void Dispose(bool _) { - _createTexture2D.Dispose(); - _renderTargetManagerInitialize.Dispose(); + _createTexture2D?.Dispose(); + _renderTargetManagerInitialize?.Dispose(); } private nint RenderTargetManagerInitializeDetour(RenderTargetManager* @this) { - _createTexture2D.Enable(); + _createTexture2D!.Enable(); _share.Item5[0] = true; _textureIndices.Value = new TextureIndices(0, 0); _textures.Value = _config.DebugMode ? [] : null; try { - return _renderTargetManagerInitialize.Original(@this); + return _renderTargetManagerInitialize!.Original(@this); } finally { @@ -133,7 +138,7 @@ public unsafe class RenderTargetHdrEnabler : IService, IDisposable _textureIndices.Value = indices; } - var texture = _createTexture2D.Original(@this, size, mipLevel, textureFormat, flags, unk); + var texture = _createTexture2D!.Original(@this, size, mipLevel, textureFormat, flags, unk); if (_textures.IsValueCreated) _textures.Value?.Add((nint)texture, (indices.CreationOrder - 1, originalTextureFormat)); return texture; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs index 5ba8c975..35ee86dc 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapHandler.cs @@ -2,9 +2,9 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; -public sealed class PapHandler(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +public sealed class PapHandler(PeSigScanner sigScanner, PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { - private readonly PapRewriter _papRewriter = new(papResourceHandler); + private readonly PapRewriter _papRewriter = new(sigScanner, papResourceHandler); public void Enable() { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 2fb1623d..5fdec816 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -7,21 +7,20 @@ using Swan; namespace Penumbra.Interop.Hooks.ResourceLoading; -public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable +public sealed class PapRewriter(PeSigScanner sigScanner, PapRewriter.PapResourceHandlerPrototype papResourceHandler) : IDisposable { public unsafe delegate int PapResourceHandlerPrototype(void* self, byte* path, int length); - private readonly PeSigScanner _scanner = new(); private readonly Dictionary _hooks = []; private readonly Dictionary<(nint, Register, ulong), nint> _nativeAllocPaths = []; private readonly List _nativeAllocCaves = []; public void Rewrite(string sig, string name) { - if (!_scanner.TryScanText(sig, out var address)) + if (!sigScanner.TryScanText(sig, out var address)) throw new Exception($"Signature for {name} [{sig}] could not be found."); - var funcInstructions = _scanner.GetFunctionInstructions(address).ToArray(); + var funcInstructions = sigScanner.GetFunctionInstructions(address).ToArray(); var hookPoints = ScanPapHookPoints(funcInstructions).ToList(); foreach (var hookPoint in hookPoints) @@ -165,8 +164,6 @@ public sealed class PapRewriter(PapRewriter.PapResourceHandlerPrototype papResou public void Dispose() { - _scanner.Dispose(); - foreach (var hook in _hooks.Values) { hook.Disable(); diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs index 4be0da00..620f3160 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PeSigScanner.cs @@ -1,12 +1,13 @@ using System.IO.MemoryMappedFiles; using Iced.Intel; +using OtterGui.Services; using PeNet; using Decoder = Iced.Intel.Decoder; namespace Penumbra.Interop.Hooks.ResourceLoading; // A good chunk of this was blatantly stolen from Dalamud's SigScanner 'cause Winter could not be faffed, Winter will definitely not rewrite it later -public unsafe class PeSigScanner : IDisposable +public unsafe class PeSigScanner : IDisposable, IService { private readonly MemoryMappedFile _file; private readonly MemoryMappedViewAccessor _textSection; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 47f96d98..ad9c41e6 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -22,7 +22,7 @@ public unsafe class ResourceLoader : IDisposable, IService private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config) + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner) { _resources = resources; _fileReadService = fileReadService; @@ -35,7 +35,7 @@ public unsafe class ResourceLoader : IDisposable, IService _resources.ResourceHandleDecRef += DecRefProtection; _fileReadService.ReadSqPack += ReadSqPackDetour; - _papHandler = new PapHandler(PapResourceHandler); + _papHandler = new PapHandler(sigScanner, PapResourceHandler); _papHandler.Enable(); } From 3687c99ee6d385af21e22371f6492f3bcb11c20c Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 11 Jan 2025 17:02:43 +0000 Subject: [PATCH 531/865] [CI] Updating repo.json for testing_1.3.3.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index ec1fc623..14c5d8ac 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.2", + "TestingAssemblyVersion": "1.3.3.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 415e15f3b150e0ee662447cbffe7340b45f50845 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 11 Jan 2025 21:12:14 +0100 Subject: [PATCH 532/865] Fix another issue with temporary mod settings. --- Penumbra/Mods/Settings/TemporaryModSettings.cs | 10 ++++++++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 6 +++--- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index 425e0348..fa71e1b6 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -20,17 +20,23 @@ public sealed class TemporaryModSettings : ModSettings public TemporaryModSettings() { } - public TemporaryModSettings(ModSettings? clone, string source, int key = 0) + public TemporaryModSettings(Mod mod, ModSettings? clone, string source, int key = 0) { Source = source; Lock = key; ForceInherit = clone == null; - if (clone != null) + if (clone != null && clone != Empty) { Enabled = clone.Enabled; Priority = clone.Priority; Settings = clone.Settings.Clone(); } + else + { + Enabled = false; + Priority = ModPriority.Default; + Settings = SettingList.Default(mod); + } } } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 280956f4..1a7d4e31 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -277,19 +277,19 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Sun, 12 Jan 2025 00:03:36 +0100 Subject: [PATCH 533/865] Add counts to multi mod selection. --- Penumbra/UI/ModsTab/MultiModPanel.cs | 90 +++++++++++++++++----------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 4079748e..0e9b5d39 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -4,65 +4,88 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Mods; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor) : IUiService +public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor) : IUiService { public void Draw() { - if (_selector.SelectedPaths.Count == 0) + if (selector.SelectedPaths.Count == 0) return; ImGui.NewLine(); - DrawModList(); + var treeNodePos = ImGui.GetCursorPos(); + var numLeaves = DrawModList(); + DrawCounts(treeNodePos, numLeaves); DrawMultiTagger(); } - private void DrawModList() + private void DrawCounts(Vector2 treeNodePos, int numLeaves) { - using var tree = ImRaii.TreeNode("Currently Selected Objects", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); - ImGui.Separator(); - if (!tree) - return; + var startPos = ImGui.GetCursorPos(); + var numFolders = selector.SelectedPaths.Count - numLeaves; + var text = (numLeaves, numFolders) switch + { + (0, 0) => string.Empty, // should not happen + (> 0, 0) => $"{numLeaves} Mods", + (0, > 0) => $"{numFolders} Folders", + _ => $"{numLeaves} Mods, {numFolders} Folders", + }; + ImGui.SetCursorPos(treeNodePos); + ImUtf8.TextRightAligned(text); + ImGui.SetCursorPos(startPos); + } - var sizeType = ImGui.GetFrameHeight(); - var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100; + private int DrawModList() + { + using var tree = ImUtf8.TreeNode("Currently Selected Objects###Selected"u8, + ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); + ImGui.Separator(); + + + if (!tree) + return selector.SelectedPaths.Count(l => l is ModFileSystem.Leaf); + + var sizeType = new Vector2(ImGui.GetFrameHeight()); + var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType.X - 4 * ImGui.GetStyle().CellPadding.X) / 100; var sizeMods = availableSizePercent * 35; var sizeFolders = availableSizePercent * 65; - using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg)) + var leaves = 0; + using (var table = ImUtf8.Table("mods"u8, 3, ImGuiTableFlags.RowBg)) { if (!table) - return; + return selector.SelectedPaths.Count(l => l is ModFileSystem.Leaf); - ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType); - ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods); - ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders); + ImUtf8.TableSetupColumn("type"u8, ImGuiTableColumnFlags.WidthFixed, sizeType.X); + ImUtf8.TableSetupColumn("mod"u8, ImGuiTableColumnFlags.WidthFixed, sizeMods); + ImUtf8.TableSetupColumn("path"u8, ImGuiTableColumnFlags.WidthFixed, sizeFolders); var i = 0; - foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p)) + foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p)) .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) { using var id = ImRaii.PushId(i++); + var (icon, text) = path is ModFileSystem.Leaf l + ? (FontAwesomeIcon.FileCircleMinus, l.Value.Name.Text) + : (FontAwesomeIcon.FolderMinus, string.Empty); ImGui.TableNextColumn(); - var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString(); - if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true)) - _selector.RemovePathFromMultiSelection(path); + if (ImUtf8.IconButton(icon, "Remove from selection."u8, sizeType)) + selector.RemovePathFromMultiSelection(path); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(fullName); + ImUtf8.DrawFrameColumn(text); + ImUtf8.DrawFrameColumn(fullName); + if (path is ModFileSystem.Leaf) + ++leaves; } } ImGui.Separator(); + return leaves; } private string _tag = string.Empty; @@ -72,11 +95,10 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito private void DrawMultiTagger() { var width = ImGuiHelpers.ScaledVector2(150, 0); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted("Multi Tagger:"); + ImUtf8.TextFrameAligned("Multi Tagger:"u8); ImGui.SameLine(); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X)); - ImGui.InputTextWithHint("##tag", "Local Tag Name...", ref _tag, 128); + ImUtf8.InputText("##tag"u8, ref _tag, "Local Tag Name..."u8); UpdateTagCache(); var label = _addMods.Count > 0 @@ -88,9 +110,9 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito : $"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(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _addMods.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _addMods.Count == 0)) foreach (var mod in _addMods) - _editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); + editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); label = _removeMods.Count > 0 ? $"Remove from {_removeMods.Count} Mods" @@ -101,9 +123,9 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito : $"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(); - if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _removeMods.Count == 0)) + if (ImUtf8.ButtonEx(label, tooltip, width, _removeMods.Count == 0)) foreach (var (mod, index) in _removeMods) - _editor.ChangeLocalTag(mod, index, string.Empty); + editor.ChangeLocalTag(mod, index, string.Empty); ImGui.Separator(); } @@ -114,7 +136,7 @@ public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _edito if (_tag.Length == 0) return; - foreach (var leaf in _selector.SelectedPaths.OfType()) + foreach (var leaf in selector.SelectedPaths.OfType()) { var index = leaf.Value.LocalTags.IndexOf(_tag); if (index >= 0) From 30a4b90e843359230bbe7a127d49da16381aeb76 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 14:34:18 +0100 Subject: [PATCH 534/865] Add IPC for querying temporary settings. --- Penumbra.Api | 2 +- Penumbra/Api/Api/TemporaryApi.cs | 56 ++++++++++++-- Penumbra/Api/IpcProviders.cs | 2 + Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 77 +++++++++++++++++++- 4 files changed, 128 insertions(+), 9 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index de0f281f..b4e716f8 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit de0f281fbf9d8d9d3aa8463a28025d54877cde8d +Subproject commit b4e716f86d94cd4d98d8f58e580ed5f619ea87ae diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index b12ce707..d951639c 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -130,11 +130,54 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } + public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, string modDirectory, + string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return (ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args), null, string.Empty); - public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, int priority, + return QueryTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + public (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source) + QueryTemporaryModSettingsPlayer(int objectIndex, + string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return (ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args), null, string.Empty); + + return QueryTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private (PenumbraApiEc ErrorCode, (bool, bool, int, Dictionary>)? Settings, string Source) QueryTemporaryModSettings( + in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return (ApiHelpers.Return(PenumbraApiEc.ModMissing, args), null, string.Empty); + + if (collection.Identity.Index <= 0) + return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty); + + var settings = collection.GetTempSettings(mod.Index); + if (settings == null) + return (ApiHelpers.Return(PenumbraApiEc.Success, args), null, string.Empty); + + if (settings.Lock > 0 && settings.Lock != key) + return (ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args), null, settings.Source); + + return (ApiHelpers.Return(PenumbraApiEc.Success, args), + (settings.ForceInherit, settings.Enabled, settings.Priority.Value, settings.ConvertToShareable(mod).Settings), settings.Source); + } + + + public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, + int priority, IReadOnlyDictionary> options, string source, int key) { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, + "Enabled", enabled, "Priority", priority, "Options", options, "Source", source, "Key", key); if (!collectionManager.Storage.ById(collectionId, out var collection)) return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); @@ -142,10 +185,12 @@ public class TemporaryApi( return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); } - public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, int priority, + public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, + int priority, IReadOnlyDictionary> options, string source, int key) { - var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", + enabled, "Priority", priority, "Options", options, "Source", source, "Key", key); if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); @@ -254,7 +299,8 @@ public class TemporaryApi( var numRemoved = 0; for (var i = 0; i < collection.Settings.Count; ++i) { - if (collection.GetTempSettings(i) is {} tempSettings && tempSettings.Lock == key + if (collection.GetTempSettings(i) is { } tempSettings + && tempSettings.Lock == key && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) ++numRemoved; } diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 6f3b2c38..f6948832 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -103,6 +103,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary), IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.QueryTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.QueryTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 832fea82..3dc8862e 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -51,7 +51,7 @@ public class TemporaryIpcTester( ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); - ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); + ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); @@ -126,12 +126,14 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); if (ImUtf8.Button("Set##SetTemporary"u8)) - _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, false, true, 1337, + new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) - _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337, new Dictionary>(), + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, false, true, 1337, + new Dictionary>(), "IPC Tester", 1337); IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); @@ -161,6 +163,75 @@ public class TemporaryIpcTester( ImGui.SameLine(); if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8)) _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338); + + IpcTester.DrawIntro(QueryTemporaryModSettings.Label, "Query Temporary Mod Settings from specific Collection"); + ImUtf8.Button("Query##QueryTemporaryModSettings"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1337); + DrawTooltip(settings, source); + } + + ImGui.SameLine(); + ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporary"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = new QueryTemporaryModSettings(pi).Invoke(guid, _modDirectory, out var settings, out var source, 1338); + DrawTooltip(settings, source); + } + + IpcTester.DrawIntro(QueryTemporaryModSettingsPlayer.Label, "Query Temporary Mod Settings from game object Collection"); + ImUtf8.Button("Query##QueryTemporaryModSettingsPlayer"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = + new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1337); + DrawTooltip(settings, source); + } + + ImGui.SameLine(); + ImUtf8.Button("Query (Wrong Key)##RemoveAllTemporaryPlayer"u8); + if (ImGui.IsItemHovered()) + { + _lastTempError = + new QueryTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, out var settings, out var source, 1338); + DrawTooltip(settings, source); + } + + void DrawTooltip((bool ForceInherit, bool Enabled, int Priority, Dictionary> Settings)? settings, string source) + { + using var tt = ImUtf8.Tooltip(); + ImUtf8.Text($"Query returned {_lastTempError}"); + if (settings != null) + ImUtf8.Text($"Settings created by {(source.Length == 0 ? "Unknown Source" : source)}:"); + else + ImUtf8.Text(source.Length > 0 ? $"Locked by {source}." : "No settings exist."); + ImGui.Separator(); + if (settings == null) + { + + return; + } + + using (ImUtf8.Group()) + { + ImUtf8.Text("Force Inherit"u8); + ImUtf8.Text("Enabled"u8); + ImUtf8.Text("Priority"u8); + foreach (var group in settings.Value.Settings.Keys) + ImUtf8.Text(group); + } + + ImGui.SameLine(); + using (ImUtf8.Group()) + { + ImUtf8.Text($"{settings.Value.ForceInherit}"); + ImUtf8.Text($"{settings.Value.Enabled}"); + ImUtf8.Text($"{settings.Value.Priority}"); + foreach (var group in settings.Value.Settings.Values) + ImUtf8.Text(string.Join("; ", group)); + } + } } public void DrawCollections() From cc981eba156e6af19c857d1d70f43b8f1afdff72 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 14:34:34 +0100 Subject: [PATCH 535/865] Fix used dye channel in material editor previews. --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index ab93dc5f..f32a3dc9 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -603,6 +603,7 @@ public partial class MtrlTab value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); ImGui.SameLine(subColWidth); ImGui.SetNextItemWidth(scalarSize); + _stainService.GudTemplateCombo.CurrentDyeChannel = dye.Channel; if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { From 9c25fab1839473da1031382f1d54d7fe7105e5ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 14:41:32 +0100 Subject: [PATCH 536/865] Increase API minor 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 894b2674..05c47644 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 4); + => (5, 5); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; From 9559bd7358d1de390811fd331aef4bac97febcc9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 14 Jan 2025 15:20:37 +0100 Subject: [PATCH 537/865] Improve RSP Identifier ToString. --- Penumbra/Meta/Manipulations/Rsp.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 2d73ec7f..9dc4fe90 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -40,6 +40,9 @@ public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attrib public MetaManipulationType Type => MetaManipulationType.Rsp; + + public override string ToString() + => $"RSP - {SubRace.ToName()} - {Attribute.ToFullString()}"; } [JsonConverter(typeof(Converter))] From e77fa18c61f5c98a6a3e9c0ce1ff0900ee6df5a3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 10 Jan 2025 15:42:23 +0100 Subject: [PATCH 538/865] Start for combining groups. --- Penumbra/Mods/Groups/CombiningModGroup.cs | 235 ++++++++++++++++++ Penumbra/Mods/Groups/IModGroup.cs | 3 +- .../OptionEditor/CombiningModGroupEditor.cs | 72 ++++++ .../Manager/OptionEditor/ModGroupEditor.cs | 55 ++-- .../Mods/SubMods/CombinedDataContainer.cs | 72 ++++++ Penumbra/Mods/SubMods/CombiningSubMod.cs | 25 ++ Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 8 +- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 13 + .../Groups/CombiningModGroupEditDrawer.cs | 11 + 9 files changed, 468 insertions(+), 26 deletions(-) create mode 100644 Penumbra/Mods/Groups/CombiningModGroup.cs create mode 100644 Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs create mode 100644 Penumbra/Mods/SubMods/CombinedDataContainer.cs create mode 100644 Penumbra/Mods/SubMods/CombiningSubMod.cs create mode 100644 Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs new file mode 100644 index 00000000..255f84aa --- /dev/null +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -0,0 +1,235 @@ +using Dalamud.Interface.ImGuiNotification; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Filesystem; +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; + +/// Groups that allow all available options to be selected at once. +public sealed class CombiningModGroup : IModGroup +{ + + public GroupType Type + => GroupType.Combining; + + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + + public Mod Mod { get; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = string.Empty; + public string Image { get; set; } = string.Empty; + public ModPriority Priority { get; set; } + public int Page { get; set; } + public Setting DefaultSettings { get; set; } + public readonly List OptionData = []; + public List Data { get; private set; } + + /// Groups that allow all available options to be selected at once. + public CombiningModGroup(Mod mod) + { + Mod = mod; + Data = [new CombinedDataContainer(this)]; + } + + IReadOnlyList IModGroup.Options + => OptionData; + + public IReadOnlyList DataContainers + => Data; + + public bool IsOption + => OptionData.Count > 0; + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + { + foreach (var path in Data.SelectWhere(o + => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))) + return path; + + return null; + } + + public void RemoveOption(int index) + { + if(index < 0 || index >= OptionData.Count) + return; + + OptionData.RemoveAt(index); + var list = new List(Data.Count / 2); + var optionFlag = 1 << index; + list.AddRange(Data.Where((c, i) => (i & optionFlag) == 0)); + Data = list; + } + + public void MoveOption(int from, int to) + { + if (!OptionData.Move(ref from, ref to)) + return; + + var list = new List(Data.Count); + for (var i = 0ul; i < (ulong)Data.Count; ++i) + { + var actualIndex = (int) Functions.MoveBit(i, from, to); + list.Add(Data[actualIndex]); + } + + Data = list; + } + + public IModOption? AddOption(string name, string description = "") + { + var groupIdx = Mod.Groups.IndexOf(this); + if (groupIdx < 0) + return null; + + var subMod = new CombiningSubMod(this) + { + Name = name, + Description = description, + }; + // Double available containers. + FillContainers(2 * Data.Count); + OptionData.Add(subMod); + return subMod; + } + + public static CombiningModGroup? Load(Mod mod, JObject json) + { + var ret = new CombiningModGroup(mod, true); + if (!ModSaveGroup.ReadJsonBase(json, ret)) + return null; + + var options = json["Options"]; + if (options != null) + foreach (var child in options.Children()) + { + if (ret.OptionData.Count == IModGroup.MaxCombiningOptions) + { + Penumbra.Messager.NotificationMessage( + $"Combining Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxCombiningOptions} options, ignoring excessive options.", + NotificationType.Warning); + break; + } + + var subMod = new CombiningSubMod(ret, child); + ret.OptionData.Add(subMod); + } + + var requiredContainers = 1 << ret.OptionData.Count; + var containers = json["Containers"]; + if (containers != null) + foreach (var child in containers.Children()) + { + if (requiredContainers <= ret.Data.Count) + { + Penumbra.Messager.NotificationMessage( + $"Combining Group {ret.Name} in {mod.Name} has more data containers than it can support with {ret.OptionData.Count} options, ignoring excessive containers.", + NotificationType.Warning); + break; + } + + var container = new CombinedDataContainer(ret, child); + ret.Data.Add(container); + } + + if (requiredContainers > ret.Data.Count) + { + Penumbra.Messager.NotificationMessage( + $"Combining Group {ret.Name} in {mod.Name} has not enough data containers for its {ret.OptionData.Count} options, filling with empty containers.", + NotificationType.Warning); + ret.FillContainers(requiredContainers); + } + + ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); + + return ret; + } + + public int GetIndex() + => ModGroup.GetIndex(this); + + public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer) + => new CombiningModGroupEditDrawer(editDrawer, this); + + public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) + => Data[setting.AsIndex].AddDataTo(redirections, manipulations); + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + foreach (var container in DataContainers) + identifier.AddChangedItems(container, changedItems); + } + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + + jWriter.WritePropertyName("Containers"); + jWriter.WriteStartArray(); + foreach (var container in Data) + { + jWriter.WriteStartObject(); + if (container.Name.Length > 0) + { + jWriter.WritePropertyName("Name"); + jWriter.WriteValue(container.Name); + } + + SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => ModGroup.GetCountsBase(this); + + public Setting FixSetting(Setting setting) + => new(Math.Min(setting.Value, (ulong)(Data.Count - 1))); + + /// Create a group without a mod only for saving it in the creator. + internal static CombiningModGroup WithoutMod(string name) + => new(null!) + { + Name = name, + }; + + /// For loading when no empty container should be created. + private CombiningModGroup(Mod mod, bool _) + { + Mod = mod; + Data = []; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void FillContainers(int requiredCount) + { + if (requiredCount <= Data.Count) + return; + + Data.EnsureCapacity(requiredCount); + Data.AddRange(Enumerable.Repeat(0, requiredCount - Data.Count).Select(_ => new CombinedDataContainer(this))); + } +} diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index a6f6e20d..96422caf 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -22,7 +22,8 @@ public enum GroupDrawBehaviour public interface IModGroup { - public const int MaxMultiOptions = 32; + public const int MaxMultiOptions = 32; + public const int MaxCombiningOptions = 8; public Mod Mod { get; } public string Name { get; set; } diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs new file mode 100644 index 00000000..46c8e3db --- /dev/null +++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs @@ -0,0 +1,72 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager.OptionEditor; + +public sealed class CombiningModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + protected override CombiningModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override CombiningSubMod? CloneOption(CombiningModGroup group, IModOption option) + { + if (group.OptionData.Count >= IModGroup.MaxCombiningOptions) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " + + $"since only up to {IModGroup.MaxCombiningOptions} options are supported in one group."); + return null; + } + + var newOption = new CombiningSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + + if (option is IModDataContainer data) + { + SubMod.Clone(data, newOption); + if (option is MultiSubMod m) + newOption.Priority = m.Priority; + else + newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); + } + + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(CombiningModGroup group, int optionIndex) + { + var optionFlag = 1 << optionIndex; + for (var i = group.Data.Count - 1; i >= 0; --i) + { + group.Data.RemoveAll() + if ((i & optionFlag) == optionFlag) + group.Data.RemoveAt(i); + } + + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + } + + protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index d01297db..b66b4d8c 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -37,6 +37,7 @@ public class ModGroupEditor( SingleModGroupEditor singleEditor, MultiModGroupEditor multiEditor, ImcModGroupEditor imcEditor, + CombiningModGroupEditor combiningEditor, CommunicatorService communicator, SaveService saveService, Configuration config) : IService @@ -50,6 +51,9 @@ public class ModGroupEditor( public ImcModGroupEditor ImcEditor => imcEditor; + public CombiningModGroupEditor CombiningEditor + => combiningEditor; + /// Change the settings stored as default options in a mod. public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption) { @@ -223,52 +227,60 @@ public class ModGroupEditor( case ImcSubMod i: ImcEditor.DeleteOption(i); return; + case CombiningModGroup c: + CombiningEditor.DeleteOption(c); + return; } } public IModOption? AddOption(IModGroup group, IModOption option) => group switch { - SingleModGroup s => SingleEditor.AddOption(s, option), - MultiModGroup m => MultiEditor.AddOption(m, option), - ImcModGroup i => ImcEditor.AddOption(i, option), - _ => null, + SingleModGroup s => SingleEditor.AddOption(s, option), + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + CombiningModGroup c => CombiningEditor.AddOption(c, option), + _ => null, }; public IModOption? AddOption(IModGroup group, string newName) => group switch { - SingleModGroup s => SingleEditor.AddOption(s, newName), - MultiModGroup m => MultiEditor.AddOption(m, newName), - ImcModGroup i => ImcEditor.AddOption(i, newName), - _ => null, + SingleModGroup s => SingleEditor.AddOption(s, newName), + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + CombiningModGroup c => CombiningEditor.AddOption(c, newName), + _ => null, }; public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) => type switch { - GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), - GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), - GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), - _ => null, + GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), + GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, default, default, saveType), + _ => null, }; public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) => type switch { - GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), - GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), - _ => (null, -1, false), + GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Combining => CombiningEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), }; public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) => group switch { - SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), - MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), - ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), - _ => (null, -1, false), + SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + CombiningModGroup c => CombiningEditor.FindOrAddOption(c, name, saveType), + _ => (null, -1, false), }; public void MoveOption(IModOption option, int toIdx) @@ -284,6 +296,9 @@ public class ModGroupEditor( case ImcSubMod i: ImcEditor.MoveOption(i, toIdx); return; + case CombiningSubMod c: + CombiningEditor.MoveOption(c, toIdx); + return; } } } diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs new file mode 100644 index 00000000..3e8ec95b --- /dev/null +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; +using Swan.Formatters; + +namespace Penumbra.Mods.SubMods; + +public class CombinedDataContainer(IModGroup group) : IModDataContainer +{ + public IMod Mod + => Group.Mod; + + public IModGroup Group { get; } = group; + + public string Name { get; } = string.Empty; + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public MetaDictionary Manipulations { get; set; } = new(); + + public void AddDataTo(Dictionary redirections, MetaDictionary manipulations) + => SubMod.AddContainerTo(this, redirections, manipulations); + + public string GetName() + { + if (Name.Length > 0) + return Name; + + var index = GetDataIndex(); + if (index == 0) + return "None"; + + var sb = new StringBuilder(128); + for (var i = 0; i < IModGroup.MaxCombiningOptions; ++i) + { + if ((index & 1) == 0) + continue; + + sb.Append(Group.Options[i].Name); + sb.Append(' ').Append('+').Append(' '); + index >>= 1; + if (index == 0) + break; + } + + return sb.ToString(0, sb.Length - 3); + } + + public string GetFullName() + => $"{Group.Name}: {GetName()}"; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } + + public CombinedDataContainer(CombiningModGroup group, JToken token) + : this(group) + { + SubMod.LoadDataContainer(token, this, group.Mod.ModPath); + Name = token["Name"]?.ToObject() ?? string.Empty; + } +} diff --git a/Penumbra/Mods/SubMods/CombiningSubMod.cs b/Penumbra/Mods/SubMods/CombiningSubMod.cs new file mode 100644 index 00000000..6eb5de9d --- /dev/null +++ b/Penumbra/Mods/SubMods/CombiningSubMod.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json.Linq; +using Penumbra.Mods.Groups; + +namespace Penumbra.Mods.SubMods; + +public class CombiningSubMod(IModGroup group) : IModOption +{ + public IModGroup Group { get; } = group; + + public Mod Mod + => Group.Mod; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; + + public string FullName + => $"{Group.Name}: {Name}"; + + public int GetIndex() + => SubMod.GetIndex(this); + + public CombiningSubMod(CombiningModGroup group, JToken json) + : this(group) + => SubMod.LoadOptionData(json, this); +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 02e945f3..7f1a8ac5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -452,19 +452,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private bool DrawOptionSelectHeader() { - const string defaultOption = "Default Option"; using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); var ret = false; - if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor.Option is DefaultSubMod)) + if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0).Wait(); ret = true; } ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) + if (ImUtf8.ButtonEx("Refresh Data"u8, "Refresh data for the current option.\nThis resets unsaved changes."u8, width)) { _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait(); ret = true; @@ -474,7 +472,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService ImGui.SetNextItemWidth(width.X); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName()); + using var combo = ImUtf8.Combo("##optionSelector"u8, _editor.Option!.GetFullName()); if (!combo) return ret; diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index c30239bc..a3e7ce14 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -48,6 +48,7 @@ public class AddGroupDrawer : IUiService DrawSingleGroupButton(mod, buttonWidth); ImUtf8.SameLineInner(); DrawMultiGroupButton(mod, buttonWidth); + DrawCombiningGroupButton(mod, buttonWidth); } private void DrawSingleGroupButton(Mod mod, Vector2 width) @@ -76,6 +77,18 @@ public class AddGroupDrawer : IUiService _groupNameValid = false; } + private void DrawCombiningGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Combining Group"u8, _groupNameValid + ? "Add a new combining option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } private void DrawImcInput(float width) { var change = ImcMetaDrawer.DrawObjectType(ref _imcIdentifier, width); diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs new file mode 100644 index 00000000..79d2fb43 --- /dev/null +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -0,0 +1,11 @@ +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + + } +} From 795fa7336e9654ff454ce79cae9387cef8858918 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 15 Jan 2025 17:44:22 +0100 Subject: [PATCH 539/865] Update with workable prototype. --- OtterGui | 2 +- Penumbra/Mods/Groups/CombiningModGroup.cs | 49 +-------- .../OptionEditor/CombiningModGroupEditor.cs | 58 +++------- .../Manager/OptionEditor/ModGroupEditor.cs | 4 +- Penumbra/Mods/ModCreator.cs | 12 +-- .../Mods/SubMods/CombinedDataContainer.cs | 12 +-- Penumbra/Mods/SubMods/SubMod.cs | 41 ++++--- .../Groups/CombiningModGroupEditDrawer.cs | 102 +++++++++++++++++- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 4 + 9 files changed, 168 insertions(+), 116 deletions(-) diff --git a/OtterGui b/OtterGui index fd387218..055f1695 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit fd387218d2d2d237075cb35be6ca89eeb53e14e5 +Subproject commit 055f169572223fd1b59389549c88b4c861c94608 diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 255f84aa..80f3c4c0 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; @@ -18,7 +17,6 @@ namespace Penumbra.Mods.Groups; /// Groups that allow all available options to be selected at once. public sealed class CombiningModGroup : IModGroup { - public GroupType Type => GroupType.Combining; @@ -60,33 +58,6 @@ public sealed class CombiningModGroup : IModGroup return null; } - public void RemoveOption(int index) - { - if(index < 0 || index >= OptionData.Count) - return; - - OptionData.RemoveAt(index); - var list = new List(Data.Count / 2); - var optionFlag = 1 << index; - list.AddRange(Data.Where((c, i) => (i & optionFlag) == 0)); - Data = list; - } - - public void MoveOption(int from, int to) - { - if (!OptionData.Move(ref from, ref to)) - return; - - var list = new List(Data.Count); - for (var i = 0ul; i < (ulong)Data.Count; ++i) - { - var actualIndex = (int) Functions.MoveBit(i, from, to); - list.Add(Data[actualIndex]); - } - - Data = list; - } - public IModOption? AddOption(string name, string description = "") { var groupIdx = Mod.Groups.IndexOf(this); @@ -98,10 +69,9 @@ public sealed class CombiningModGroup : IModGroup Name = name, Description = description, }; - // Double available containers. - FillContainers(2 * Data.Count); - OptionData.Add(subMod); - return subMod; + return OptionData.AddNewWithPowerSet(Data, subMod, () => new CombinedDataContainer(this), IModGroup.MaxCombiningOptions) + ? subMod + : null; } public static CombiningModGroup? Load(Mod mod, JObject json) @@ -148,7 +118,8 @@ public sealed class CombiningModGroup : IModGroup Penumbra.Messager.NotificationMessage( $"Combining Group {ret.Name} in {mod.Name} has not enough data containers for its {ret.OptionData.Count} options, filling with empty containers.", NotificationType.Warning); - ret.FillContainers(requiredContainers); + ret.Data.EnsureCapacity(requiredContainers); + ret.Data.AddRange(Enumerable.Repeat(0, requiredContainers - ret.Data.Count).Select(_ => new CombinedDataContainer(ret))); } ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); @@ -222,14 +193,4 @@ public sealed class CombiningModGroup : IModGroup Mod = mod; Data = []; } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void FillContainers(int requiredCount) - { - if (requiredCount <= Data.Count) - return; - - Data.EnsureCapacity(requiredCount); - Data.AddRange(Enumerable.Repeat(0, requiredCount - Data.Count).Select(_ => new CombinedDataContainer(this))); - } } diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs index 46c8e3db..ce5db454 100644 --- a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs @@ -1,5 +1,5 @@ +using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; @@ -19,54 +19,30 @@ public sealed class CombiningModGroupEditor(CommunicatorService communicator, Sa }; protected override CombiningSubMod? CloneOption(CombiningModGroup group, IModOption option) - { - if (group.OptionData.Count >= IModGroup.MaxCombiningOptions) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " - + $"since only up to {IModGroup.MaxCombiningOptions} options are supported in one group."); - return null; - } - - var newOption = new CombiningSubMod(group) - { - Name = option.Name, - Description = option.Description, - }; - - if (option is IModDataContainer data) - { - SubMod.Clone(data, newOption); - if (option is MultiSubMod m) - newOption.Priority = m.Priority; - else - newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); - } - - group.OptionData.Add(newOption); - return newOption; - } + => throw new NotImplementedException(); protected override void RemoveOption(CombiningModGroup group, int optionIndex) { - var optionFlag = 1 << optionIndex; - for (var i = group.Data.Count - 1; i >= 0; --i) - { - group.Data.RemoveAll() - if ((i & optionFlag) == optionFlag) - group.Data.RemoveAt(i); - } - - group.OptionData.RemoveAt(optionIndex); - group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + if (group.OptionData.RemoveWithPowerSet(group.Data, optionIndex)) + group.DefaultSettings.RemoveBit(optionIndex); } - protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + protected override bool MoveOption(CombiningModGroup group, int optionIdxFrom, int optionIdxTo) { - if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo)) + if (!group.OptionData.MoveWithPowerSet(group.Data, ref optionIdxFrom, ref optionIdxTo)) return false; - group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); return true; } + + public void SetDisplayName(CombinedDataContainer container, string name, SaveType saveType = SaveType.Queue) + { + if (container.Name == name) + return; + + container.Name = name; + SaveService.Save(saveType, new ModSaveGroup(container.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, container.Group.Mod, container.Group, null, null, -1); + } } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs index b66b4d8c..1c077c58 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs @@ -227,7 +227,7 @@ public class ModGroupEditor( case ImcSubMod i: ImcEditor.DeleteOption(i); return; - case CombiningModGroup c: + case CombiningSubMod c: CombiningEditor.DeleteOption(c); return; } @@ -259,7 +259,7 @@ public class ModGroupEditor( GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType), - GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, default, default, saveType), + GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, saveType), _ => null, }; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index bdc16b72..18d2bc09 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -82,13 +82,11 @@ public partial class ModCreator( if (incorporateMetaChanges) IncorporateAllMetaChanges(mod, true); if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges) - { foreach (var container in mod.AllDataContainers) { if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations)) saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); } - } return true; } @@ -186,7 +184,8 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, + bool deleteDefault) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -447,9 +446,10 @@ public partial class ModCreator( var json = JObject.Parse(File.ReadAllText(file.FullName)); switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single) { - case GroupType.Multi: return MultiModGroup.Load(mod, json); - case GroupType.Single: return SingleModGroup.Load(mod, json); - case GroupType.Imc: return ImcModGroup.Load(mod, json); + case GroupType.Multi: return MultiModGroup.Load(mod, json); + case GroupType.Single: return SingleModGroup.Load(mod, json); + case GroupType.Imc: return ImcModGroup.Load(mod, json); + case GroupType.Combining: return CombiningModGroup.Load(mod, json); } } catch (Exception e) diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs index 3e8ec95b..2c410c1c 100644 --- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -4,7 +4,6 @@ using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.String.Classes; -using Swan.Formatters; namespace Penumbra.Mods.SubMods; @@ -15,7 +14,7 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer public IModGroup Group { get; } = group; - public string Name { get; } = string.Empty; + public string Name { get; set; } = string.Empty; public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public MetaDictionary Manipulations { get; set; } = new(); @@ -35,11 +34,12 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer var sb = new StringBuilder(128); for (var i = 0; i < IModGroup.MaxCombiningOptions; ++i) { - if ((index & 1) == 0) - continue; + if ((index & 1) != 0) + { + sb.Append(Group.Options[i].Name); + sb.Append(' ').Append('+').Append(' '); + } - sb.Append(Group.Options[i].Name); - sb.Append(' ').Append('+').Append(' '); index >>= 1; if (index == 0) break; diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index f6b1be96..7f01884d 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -81,29 +81,40 @@ public static class SubMod [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { - j.WritePropertyName(nameof(data.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.Files) + if (data.Files.Count > 0) { - if (file.ToRelPath(basePath, out var relPath)) + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + } + + if (data.FileSwaps.Count > 0) + { + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) { j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); + j.WriteValue(file.ToString()); } + + j.WriteEndObject(); } - j.WriteEndObject(); - j.WritePropertyName(nameof(data.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in data.FileSwaps) + if (data.Manipulations.Count > 0) { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); } - - j.WriteEndObject(); - j.WritePropertyName(nameof(data.Manipulations)); - serializer.Serialize(j, data.Manipulations); } /// Write the data for a selectable mod option on a JsonWriter. diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index 79d2fb43..f32e6da6 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,4 +1,10 @@ +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab.Groups; @@ -6,6 +12,100 @@ public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, Co { public void Draw() { - + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImUtf8.PushId(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + editor.DrawOptionName(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + editor.DrawOptionDelete(option); + } + + DrawNewOption(); + DrawContainerNames(); + } + + private void DrawNewOption() + { + var count = group.OptionData.Count; + if (count >= IModGroup.MaxCombiningOptions) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, default, !validName)) + { + editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } + + private unsafe void DrawContainerNames() + { + if (ImUtf8.ButtonEx("Edit Container Names"u8, + "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8, + new Vector2(400 * ImUtf8.GlobalScale, 0))) + ImUtf8.OpenPopup("DataContainerNames"u8); + + var sizeX = group.OptionData.Count * (ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight()) + 300 * ImUtf8.GlobalScale; + ImGui.SetNextWindowSize(new Vector2(sizeX, ImGui.GetFrameHeightWithSpacing() * Math.Min(16, group.Data.Count) + 200 * ImUtf8.GlobalScale)); + using var popup = ImUtf8.Popup("DataContainerNames"u8); + if (!popup) + return; + + foreach (var option in group.OptionData) + { + ImUtf8.RotatedText(option.Name, true); + ImUtf8.SameLineInner(); + } + + ImGui.NewLine(); + ImGui.Separator(); + using var child = ImUtf8.Child("##Child"u8, ImGui.GetContentRegionAvail()); + ImGuiClip.ClippedDraw(group.Data, DrawRow, ImGui.GetFrameHeightWithSpacing()); + } + + private void DrawRow(CombinedDataContainer container, int index) + { + using var id = ImUtf8.PushId(index); + using (ImRaii.Disabled()) + { + for (var i = 0; i < group.OptionData.Count; ++i) + { + id.Push(i); + var check = (index & (1 << i)) != 0; + ImUtf8.Checkbox(""u8, ref check); + ImUtf8.SameLineInner(); + id.Pop(); + } + } + + var name = editor.CombiningDisplayIndex == index ? editor.CombiningDisplayName ?? container.Name : container.Name; + if (ImUtf8.InputText("##Nothing"u8, ref name, "Optional Display Name..."u8)) + { + editor.CombiningDisplayIndex = index; + editor.CombiningDisplayName = name; + } + + if (ImGui.IsItemDeactivatedAfterEdit()) + editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, name); + + if (ImGui.IsItemDeactivated()) + { + editor.CombiningDisplayIndex = -1; + editor.CombiningDisplayName = null; + } } } diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index ec5bb920..89812346 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -58,6 +58,9 @@ public sealed class ModGroupEditDrawer( private IModOption? _dragDropOption; private bool _draggingAcross; + internal string? CombiningDisplayName; + internal int CombiningDisplayIndex; + public void Draw(Mod mod) { PrepareStyle(); @@ -275,6 +278,7 @@ public sealed class ModGroupEditDrawer( [MethodImpl(MethodImplOptions.AggressiveInlining)] internal string DrawNewOptionBase(IModGroup group, int count) { + ImGui.AlignTextToFramePadding(); ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable); Target(group, count); From 1462891bd36fb0e467b5a0b9181a72d97281c211 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 15 Jan 2025 17:05:31 +0000 Subject: [PATCH 540/865] [CI] Updating repo.json for testing_1.3.3.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 14c5d8ac..d9d597ba 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.3", + "TestingAssemblyVersion": "1.3.3.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From bdc2da95c4ece4ce36b99fba9f1b9fea11b83251 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Jan 2025 17:22:25 +0100 Subject: [PATCH 541/865] Make mods write empty containers again for now. --- Penumbra.GameData | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index d5f92966..78ce195c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d5f929664c212804594fadb4e4cefe9e6a1f5d37 +Subproject commit 78ce195c171d7bce4ad9df105f1f95cce9bf1150 diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index 7f01884d..fcb6cc0e 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -81,8 +81,9 @@ public static class SubMod [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { - if (data.Files.Count > 0) - { + // #TODO: remove comments when TexTools updated. + //if (data.Files.Count > 0) + //{ j.WritePropertyName(nameof(data.Files)); j.WriteStartObject(); foreach (var (gamePath, file) in data.Files) @@ -95,10 +96,10 @@ public static class SubMod } j.WriteEndObject(); - } + //} - if (data.FileSwaps.Count > 0) - { + //if (data.FileSwaps.Count > 0) + //{ j.WritePropertyName(nameof(data.FileSwaps)); j.WriteStartObject(); foreach (var (gamePath, file) in data.FileSwaps) @@ -108,13 +109,13 @@ public static class SubMod } j.WriteEndObject(); - } + //} - if (data.Manipulations.Count > 0) - { + //if (data.Manipulations.Count > 0) + //{ j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); - } + //} } /// Write the data for a selectable mod option on a JsonWriter. From df148b556a8a8b2f90b700a01a9ac0622ace3e31 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 16 Jan 2025 16:25:18 +0000 Subject: [PATCH 542/865] [CI] Updating repo.json for testing_1.3.3.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index d9d597ba..2e3dd8bc 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.4", + "TestingAssemblyVersion": "1.3.3.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a1931a93fb4bfad042b1235ec62270f4c74e3fbc Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 01:45:37 +0100 Subject: [PATCH 543/865] Add drafts of JSON schemas --- schemas/container.json | 486 +++++++++++++++++++++++++++++++++++++++ schemas/default_mod.json | 25 ++ schemas/group.json | 206 +++++++++++++++++ schemas/meta-v3.json | 51 ++++ 4 files changed, 768 insertions(+) create mode 100644 schemas/container.json create mode 100644 schemas/default_mod.json create mode 100644 schemas/group.json create mode 100644 schemas/meta-v3.json diff --git a/schemas/container.json b/schemas/container.json new file mode 100644 index 00000000..5798f46c --- /dev/null +++ b/schemas/container.json @@ -0,0 +1,486 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", + "type": "object", + "properties": { + "Name": { + "description": "Name of the container/option/sub-mod.", + "type": ["string", "null"] + }, + "Files": { + "description": "File redirections in this container. Keys are game paths, values are relative file paths.", + "type": "object", + "patternProperties": { + "^[^/\\\\][^\\\\]*$": { + "type": "string", + "pattern": "^[^/\\\\][^/]*$" + } + }, + "additionalProperties": false + }, + "FileSwaps": { + "description": "File swaps in this container. Keys are original game paths, values are actual game paths.", + "type": "object", + "patternProperties": { + "^[^/\\\\][^\\\\]*$": { + "type": "string", + "pattern": "^[^/\\\\][^/]*$" + } + }, + "additionalProperties": false + }, + "Manipulations": { + "type": "array", + "items": { + "$ref": "#/$defs/Manipulation" + } + } + }, + "$defs": { + "Manipulation": { + "type": "object", + "properties": { + "Type": { + "enum": ["Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch"] + }, + "Manipulation": { + "type": ["object", "null"] + } + }, + "required": ["Type", "Manipulation"], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Unknown" + }, + "Manipulation": { + "type": "null" + } + } + }, { + "properties": { + "Type": { + "const": "Imc" + }, + "Manipulation": { + "$ref": "#/$defs/ImcManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Eqdp" + }, + "Manipulation": { + "$ref": "#/$defs/EqdpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Eqp" + }, + "Manipulation": { + "$ref": "#/$defs/EqpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Est" + }, + "Manipulation": { + "$ref": "#/$defs/EstManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Gmp" + }, + "Manipulation": { + "$ref": "#/$defs/GmpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Rsp" + }, + "Manipulation": { + "$ref": "#/$defs/RspManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "GlobalEqp" + }, + "Manipulation": { + "$ref": "#/$defs/GlobalEqpManipulation" + } + } + }, { + "properties": { + "Type": { + "const": "Atch" + }, + "Manipulation": { + "$ref": "#/$defs/AtchManipulation" + } + } + } + ], + "additionalProperties": false + }, + "ImcManipulation": { + "type": "object", + "properties": { + "Entry": { + "$ref": "#/$defs/ImcEntry" + }, + "Valid": { + "type": "boolean" + } + }, + "required": [ + "Entry" + ], + "allOf": [ + { + "$ref": "#/$defs/ImcIdentifier" + } + ], + "unevaluatedProperties": false + }, + "ImcIdentifier": { + "type": "object", + "properties": { + "PrimaryId": { + "type": "integer" + }, + "SecondaryId": { + "type": "integer" + }, + "Variant": { + "type": "integer" + }, + "ObjectType": { + "$ref": "#/$defs/ObjectType" + }, + "EquipSlot": { + "$ref": "#/$defs/EquipSlot" + }, + "BodySlot": { + "$ref": "#/$defs/BodySlot" + } + }, + "required": [ + "PrimaryId", + "SecondaryId", + "Variant", + "ObjectType", + "EquipSlot", + "BodySlot" + ] + }, + "ImcEntry": { + "type": "object", + "properties": { + "AttributeAndSound": { + "type": "integer" + }, + "MaterialId": { + "type": "integer" + }, + "DecalId": { + "type": "integer" + }, + "VfxId": { + "type": "integer" + }, + "MaterialAnimationId": { + "type": "integer" + }, + "AttributeMask": { + "type": "integer" + }, + "SoundId": { + "type": "integer" + } + }, + "required": [ + "MaterialId", + "DecalId", + "VfxId", + "MaterialAnimationId", + "AttributeMask", + "SoundId" + ], + "additionalProperties": false + }, + "EqdpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "$ref": "#/$defs/EquipSlot" + }, + "ShiftedEntry": { + "type": "integer" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "EqpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "$ref": "#/$defs/EquipSlot" + } + }, + "required": [ + "Entry", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "EstManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + }, + "Slot": { + "enum": ["Hair", "Face", "Body", "Head"] + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ], + "additionalProperties": false + }, + "GmpManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "Animated": { + "type": "boolean" + }, + "RotationA": { + "type": "number" + }, + "RotationB": { + "type": "number" + }, + "RotationC": { + "type": "number" + }, + "UnknownA": { + "type": "number" + }, + "UnknownB": { + "type": "number" + }, + "UnknownTotal": { + "type": "number" + }, + "Value": { + "type": "number" + } + }, + "required": [ + "Enabled", + "Animated", + "RotationA", + "RotationB", + "RotationC", + "UnknownA", + "UnknownB", + "UnknownTotal", + "Value" + ], + "additionalProperties": false + }, + "SetId": { + "$ref": "#/$defs/LaxInteger" + } + }, + "required": [ + "Entry", + "SetId" + ], + "additionalProperties": false + }, + "RspManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "number" + }, + "SubRace": { + "$ref": "#/$defs/SubRace" + }, + "Attribute": { + "$ref": "#/$defs/RspAttribute" + } + }, + "additionalProperties": false + }, + "GlobalEqpManipulation": { + "type": "object", + "properties": { + "Condition": { + "type": "integer" + }, + "Type": { + "enum": ["DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats"] + } + }, + "additionalProperties": false + }, + "AtchManipulation": { + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Bone": { + "type": "string", + "maxLength": 34 + }, + "Scale": { + "type": "number" + }, + "OffsetX": { + "type": "number" + }, + "OffsetY": { + "type": "number" + }, + "OffsetZ": { + "type": "number" + }, + "RotationX": { + "type": "number" + }, + "RotationY": { + "type": "number" + }, + "RotationZ": { + "type": "number" + } + }, + "required": [ + "Bone", + "Scale", + "OffsetX", + "OffsetY", + "OffsetZ", + "RotationX", + "RotationY", + "RotationZ" + ], + "additionalProperties": false + }, + "Gender": { + "$ref": "#/$defs/Gender" + }, + "Race": { + "$ref": "#/$defs/ModelRace" + }, + "Type": { + "type": "string", + "minLength": 3, + "maxLength": 3 + }, + "Index": { + "type": "integer" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "Type", + "Index" + ], + "additionalProperties": false + }, + "LaxInteger": { + "oneOf": [ + { + "type": "integer" + }, { + "type": "string", + "pattern": "^\\d+$" + } + ] + }, + "EquipSlot": { + "enum": ["Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All"] + }, + "Gender": { + "enum": ["Unknown", "Male", "Female", "MaleNpc", "FemaleNpc"] + }, + "ModelRace": { + "enum": ["Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera"] + }, + "ObjectType": { + "enum": ["Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font"] + }, + "BodySlot": { + "enum": ["Unknown", "Hair", "Face", "Tail", "Body", "Zear"] + }, + "SubRace": { + "enum": ["Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena"] + }, + "RspAttribute": { + "enum": ["MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ"] + } + } +} diff --git a/schemas/default_mod.json b/schemas/default_mod.json new file mode 100644 index 00000000..eecd74d0 --- /dev/null +++ b/schemas/default_mod.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", + "allOf": [ + { + "type": "object", + "properties": { + "Version": { + "description": "???", + "type": ["integer", "null"] + }, + "Description": { + "description": "Description of the sub-mod.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] +} diff --git a/schemas/group.json b/schemas/group.json new file mode 100644 index 00000000..0078e9f3 --- /dev/null +++ b/schemas/group.json @@ -0,0 +1,206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/group.json", + "type": "object", + "properties": { + "Version": { + "description": "???", + "type": ["integer", "null"] + }, + "Name": { + "description": "Name of the group.", + "type": "string" + }, + "Description": { + "description": "Description of the group.", + "type": ["string", "null"] + }, + "Image": { + "description": "Relative path to a preview image for the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", + "type": ["string", "null"] + }, + "Page": { + "description": "TexTools page of the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", + "type": ["integer", "null"] + }, + "Priority": { + "description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.", + "type": ["integer", "null"] + }, + "Type": { + "description": "Group type. Single groups have one and only one of their options active at any point. Multi groups can have zero, one or many of their options active. Combining groups have n options, 2^n containers, and will have one and only one container active depending on the selected options.", + "enum": ["Single", "Multi", "Imc", "Combining"] + }, + "DefaultSettings": { + "description": "Default configuration for the group.", + "type": "integer" + } + }, + "required": [ + "Name", + "Type" + ], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Single" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] + } + } + } + }, { + "properties": { + "Type": { + "const": "Multi" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Priority": { + "description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.", + "type": ["integer", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + } + }, { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + ] + } + } + } + }, { + "properties": { + "Type": { + "const": "Imc" + }, + "AllVariants": { + "type": "boolean" + }, + "OnlyAttributes": { + "type": "boolean" + }, + "Identifier": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcIdentifier" + }, + "DefaultEntry": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcEntry" + }, + "Options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string" + }, + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + }, + "required": [ + "Name" + ], + "oneOf": [ + { + "properties": { + "AttributeMask": { + "type": "integer" + } + }, + "required": [ + "AttributeMask" + ] + }, { + "properties": { + "IsDisableSubMod": { + "const": true + } + }, + "required": [ + "IsDisableSubMod" + ] + } + ], + "unevaluatedProperties": false + } + } + } + }, { + "properties": { + "Type": { + "const": "Combining" + }, + "Options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string" + }, + "Description": { + "description": "Description of the option.", + "type": ["string", "null"] + }, + "Image": { + "description": "Unused by Penumbra.", + "type": ["string", "null"] + } + }, + "required": [ + "Name" + ], + "additionalProperties": false + } + }, + "Containers": { + "type": "array", + "items": { + "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + } + } + } + } + ], + "unevaluatedProperties": false +} diff --git a/schemas/meta-v3.json b/schemas/meta-v3.json new file mode 100644 index 00000000..1a132264 --- /dev/null +++ b/schemas/meta-v3.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/meta-v3.json", + "title": "Penumbra Mod Metadata", + "description": "Metadata of a Penumbra mod.", + "type": "object", + "properties": { + "FileVersion": { + "description": "Major version of the metadata schema used.", + "type": "integer", + "minimum": 3, + "maximum": 3 + }, + "Name": { + "description": "Name of the mod.", + "type": "string" + }, + "Author": { + "description": "Author of the mod.", + "type": ["string", "null"] + }, + "Description": { + "description": "Description of the mod. Can span multiple paragraphs.", + "type": ["string", "null"] + }, + "Image": { + "description": "Relative path to a preview image for the mod. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", + "type": ["string", "null"] + }, + "Version": { + "description": "Version of the mod. Can be an arbitrary string.", + "type": ["string", "null"] + }, + "Website": { + "description": "URL of the web page of the mod.", + "type": ["string", "null"] + }, + "ModTags": { + "description": "Author-defined tags for the mod.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "FileVersion", + "Name" + ] +} From 3b8aac8eca94e8253b357a8b885a56bb7919e7d2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 18:50:00 +0100 Subject: [PATCH 544/865] Add schema for Material Development Kit files --- schemas/shpk_devkit.json | 500 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 schemas/shpk_devkit.json diff --git a/schemas/shpk_devkit.json b/schemas/shpk_devkit.json new file mode 100644 index 00000000..cd18ab81 --- /dev/null +++ b/schemas/shpk_devkit.json @@ -0,0 +1,500 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/shpk_devkit.json", + "type": "object", + "properties": { + "ShaderKeys": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/ShaderKey" + } + }, + "additionalProperties": false + }, + "Comment": { + "$ref": "#/$defs/MayVary" + }, + "Samplers": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/MayVary" + } + }, + "additionalProperties": false + }, + "Constants": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/MayVary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "ShaderKeyValue": { + "type": "object", + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ShaderKey": { + "type": "object", + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Values": { + "type": "object", + "patternProperties": { + "^\\d+$": { + "$ref": "#/$defs/ShaderKeyValue" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "Varying": { + "type": "object", + "properties": { + "Vary": { + "type": "array", + "items": { + "$ref": "#/$defs/LaxInteger" + } + }, + "Selectors": { + "description": "Keys are Σ 31^i shaderKey(Vary[i]).", + "type": "object", + "patternProperties": { + "^\\d+$": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "Items": { + "type": "array", + "$comment": "Varying is defined by constraining this array's items to T" + } + }, + "required": [ + "Vary", + "Selectors", + "Items" + ], + "additionalProperties": false + }, + "MayVary": { + "oneOf": [ + { + "type": ["string", "null"] + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "type": ["string", "null"] + } + } + } + } + ] + } + ] + }, + "Sampler": { + "type": ["object", "null"], + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "DefaultTexture": { + "type": "string", + "pattern": "^[^/\\\\][^\\\\]*$" + } + }, + "additionalProperties": false + }, + "MayVary": { + "oneOf": [ + { + "$ref": "#/$defs/Sampler" + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "$ref": "#/$defs/Sampler" + } + } + } + } + ] + } + ] + }, + "ConstantBase": { + "type": "object", + "properties": { + "Offset": { + "description": "Defaults to 0. Mutually exclusive with ByteOffset.", + "type": "integer", + "minimum": 0 + }, + "Length": { + "description": "Defaults to up to the end. Mutually exclusive with ByteLength.", + "type": "integer", + "minimum": 0 + }, + "ByteOffset": { + "description": "Defaults to 0. Mutually exclusive with Offset.", + "type": "integer", + "minimum": 0 + }, + "ByteLength": { + "description": "Defaults to up to the end. Mutually exclusive with Length.", + "type": "integer", + "minimum": 0 + }, + "Group": { + "description": "Defaults to \"Further Constants\".", + "type": "string" + }, + "Label": { + "type": "string" + }, + "Description": { + "description": "Defaults to empty.", + "type": "string" + }, + "Type": { + "description": "Defaults to Float.", + "enum": ["Hidden", "Float", "Integer", "Color", "Enum", "Int32", "Int32Enum", "Int8", "Int8Enum", "Int16", "Int16Enum", "Int64", "Int64Enum", "Half", "Double", "TileIndex", "SphereMapIndex"] + } + }, + "not": { + "anyOf": [ + { + "required": ["Offset", "ByteOffset"] + }, { + "required": ["Length", "ByteLenngth"] + } + ] + } + }, + "HiddenConstant": { + "type": "object", + "properties": { + "Type": { + "const": "Hidden" + } + }, + "required": [ + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "FloatConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["Float", "Half", "Double"] + }, + "Minimum": { + "description": "Defaults to -∞.", + "type": "number" + }, + "Maximum": { + "description": "Defaults to ∞.", + "type": "number" + }, + "Speed": { + "description": "Defaults to 0.1.", + "type": "number", + "minimum": 0 + }, + "RelativeSpeed": { + "description": "Defaults to 0.", + "type": "number", + "minimum": 0 + }, + "Exponent": { + "description": "Defaults to 1. Uses an odd pseudo-power function, f(x) = sgn(x) |x|^n.", + "type": "number" + }, + "Factor": { + "description": "Defaults to 1.", + "type": "number" + }, + "Bias": { + "description": "Defaults to 0.", + "type": "number" + }, + "Precision": { + "description": "Defaults to 3.", + "type": "integer", + "minimum": 0, + "maximum": 9 + }, + "Slider": { + "description": "Defaults to true. Drag has priority over this.", + "type": "boolean" + }, + "Drag": { + "description": "Defaults to true. Has priority over Slider.", + "type": "boolean" + }, + "Unit": { + "description": "Defaults to no unit.", + "type": "string" + } + }, + "required": [ + "Label" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "IntConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["Integer", "Int32", "Int8", "Int16", "Int64"] + }, + "Minimum": { + "description": "Defaults to -2^N, N being the explicit integer width specified in the type, or 32 for Int.", + "type": "number" + }, + "Maximum": { + "description": "Defaults to 2^N - 1, N being the explicit integer width specified in the type, or 32 for Int.", + "type": "number" + }, + "Speed": { + "description": "Defaults to 0.25.", + "type": "number", + "minimum": 0 + }, + "RelativeSpeed": { + "description": "Defaults to 0.", + "type": "number", + "minimum": 0 + }, + "Factor": { + "description": "Defaults to 1.", + "type": "number" + }, + "Bias": { + "description": "Defaults to 0.", + "type": "number" + }, + "Hex": { + "description": "Defaults to false. Has priority over Slider and Drag.", + "type": "boolean" + }, + "Slider": { + "description": "Defaults to true. Hex and Drag have priority over this.", + "type": "boolean" + }, + "Drag": { + "description": "Defaults to true. Has priority over Slider, but Hex has priority over this.", + "type": "boolean" + }, + "Unit": { + "description": "Defaults to no unit.", + "type": "string" + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "ColorConstant": { + "type": "object", + "properties": { + "Type": { + "const": "Color" + }, + "SquaredRgb": { + "description": "Defaults to false. Uses an odd pseudo-square function, f(x) = sgn(x) |x|².", + "type": "boolean" + }, + "Clamped": { + "description": "Defaults to false.", + "type": "boolean" + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "EnumValue": { + "type": "object", + "properties": { + "Label": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "Value": { + "type": "number" + } + }, + "required": [ + "Label", + "Value" + ], + "additionalProperties": false + }, + "EnumConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["Enum", "Int32Enum", "Int8Enum", "Int16Enum", "Int64Enum"] + }, + "Values": { + "type": "array", + "items": { + "$ref": "#/$defs/EnumValue" + } + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "SpecialConstant": { + "type": "object", + "properties": { + "Type": { + "enum": ["TileIndex", "SphereMapIndex"] + } + }, + "required": [ + "Label", + "Type" + ], + "allOf": [ + { + "$ref": "#/$defs/ConstantBase" + } + ], + "unevaluatedProperties": false + }, + "Constant": { + "oneOf": [ + { + "$ref": "#/$defs/HiddenConstant" + }, { + "$ref": "#/$defs/FloatConstant" + }, { + "$ref": "#/$defs/IntConstant" + }, { + "$ref": "#/$defs/ColorConstant" + }, { + "$ref": "#/$defs/EnumConstant" + }, { + "$ref": "#/$defs/SpecialConstant" + } + ] + }, + "MayVary": { + "oneOf": [ + { + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/Constant" + } + }, { + "allOf": [ + { + "$ref": "#/$defs/Varying" + }, { + "type": "object", + "properties": { + "Items": { + "type": "array", + "items": { + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/Constant" + } + } + } + } + } + ] + } + ] + }, + "LaxInteger": { + "oneOf": [ + { + "type": "integer" + }, { + "type": "string", + "pattern": "^\\d+$" + } + ] + } + } +} From 5f8377acaaf1bc20944af85e525b09313285272c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jan 2025 19:54:40 +0100 Subject: [PATCH 545/865] Update mod loading structure. --- Penumbra.GameData | 2 +- Penumbra/Mods/Manager/ModDataEditor.cs | 145 +------------------------ Penumbra/Mods/ModCreator.cs | 4 +- Penumbra/Mods/ModLocalData.cs | 57 ++++++++++ Penumbra/Mods/ModMeta.cs | 83 ++++++++++++++ 5 files changed, 146 insertions(+), 145 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 78ce195c..c5250722 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 78ce195c171d7bce4ad9df105f1f95cce9bf1150 +Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6 diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 162f823d..7c48205a 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -27,6 +27,9 @@ public enum ModDataChangeType : ushort public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService { + public SaveService SaveService + => saveService; + /// 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) @@ -40,148 +43,6 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic saveService.ImmediateSaveSync(new ModMeta(mod)); } - public ModDataChangeType LoadLocalData(Mod mod) - { - var dataFile = saveService.FileNames.LocalDataFile(mod); - - var importDate = 0L; - var localTags = Enumerable.Empty(); - var favorite = false; - var note = string.Empty; - - var save = true; - if (File.Exists(dataFile)) - try - { - var text = File.ReadAllText(dataFile); - var json = JObject.Parse(text); - - importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; - favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; - note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; - save = false; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not load local mod data:\n{e}"); - } - - if (importDate == 0) - importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - ModDataChangeType changes = 0; - if (mod.ImportDate != importDate) - { - mod.ImportDate = importDate; - changes |= ModDataChangeType.ImportDate; - } - - changes |= ModLocalData.UpdateTags(mod, null, localTags); - - if (mod.Favorite != favorite) - { - mod.Favorite = favorite; - changes |= ModDataChangeType.Favorite; - } - - if (mod.Note != note) - { - mod.Note = note; - changes |= ModDataChangeType.Note; - } - - if (save) - saveService.QueueSave(new ModLocalData(mod)); - - return changes; - } - - public ModDataChangeType LoadMeta(ModCreator creator, Mod mod) - { - var metaFile = saveService.FileNames.ModMetaPath(mod); - if (!File.Exists(metaFile)) - { - Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); - return ModDataChangeType.Deletion; - } - - try - { - var text = File.ReadAllText(metaFile); - var json = JObject.Parse(text); - - var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; - var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; - var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; - var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; - var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; - var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; - var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; - var importDate = json[nameof(Mod.ImportDate)]?.Value(); - var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); - - ModDataChangeType changes = 0; - if (mod.Name != newName) - { - changes |= ModDataChangeType.Name; - mod.Name = newName; - } - - if (mod.Author != newAuthor) - { - changes |= ModDataChangeType.Author; - mod.Author = newAuthor; - } - - if (mod.Description != newDescription) - { - changes |= ModDataChangeType.Description; - mod.Description = newDescription; - } - - if (mod.Image != newImage) - { - changes |= ModDataChangeType.Image; - mod.Image = newImage; - } - - if (mod.Version != newVersion) - { - changes |= ModDataChangeType.Version; - mod.Version = newVersion; - } - - if (mod.Website != newWebsite) - { - changes |= ModDataChangeType.Website; - mod.Website = newWebsite; - } - - if (newFileVersion != ModMeta.FileVersion) - if (ModMigration.Migrate(creator, saveService, mod, json, ref newFileVersion)) - { - changes |= ModDataChangeType.Migration; - saveService.ImmediateSave(new ModMeta(mod)); - } - - if (importDate != null && mod.ImportDate != importDate.Value) - { - mod.ImportDate = importDate.Value; - changes |= ModDataChangeType.ImportDate; - } - - changes |= ModLocalData.UpdateTags(mod, modTags, null); - - return changes; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}"); - return ModDataChangeType.Deletion; - } - } - public void ChangeModName(Mod mod, string newName) { if (mod.Name.Text == newName) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 18d2bc09..0db83ef9 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -72,11 +72,11 @@ public partial class ModCreator( if (!Directory.Exists(mod.ModPath.FullName)) return false; - modDataChange = dataEditor.LoadMeta(this, mod); + modDataChange = ModMeta.Load(dataEditor, this, mod); if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) return false; - modDataChange |= dataEditor.LoadLocalData(mod); + modDataChange |= ModLocalData.Load(dataEditor, mod); LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index beda0dc7..d3534391 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -27,6 +27,63 @@ public readonly struct ModLocalData(Mod mod) : ISavable jObject.WriteTo(jWriter); } + public static ModDataChangeType Load(ModDataEditor editor, Mod mod) + { + var dataFile = editor.SaveService.FileNames.LocalDataFile(mod); + + var importDate = 0L; + var localTags = Enumerable.Empty(); + var favorite = false; + var note = string.Empty; + + var save = true; + if (File.Exists(dataFile)) + try + { + var text = File.ReadAllText(dataFile); + var json = JObject.Parse(text); + + importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; + save = false; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not load local mod data:\n{e}"); + } + + if (importDate == 0) + importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + ModDataChangeType changes = 0; + if (mod.ImportDate != importDate) + { + mod.ImportDate = importDate; + changes |= ModDataChangeType.ImportDate; + } + + changes |= ModLocalData.UpdateTags(mod, null, localTags); + + if (mod.Favorite != favorite) + { + mod.Favorite = favorite; + changes |= ModDataChangeType.Favorite; + } + + if (mod.Note != note) + { + mod.Note = note; + changes |= ModDataChangeType.Note; + } + + if (save) + editor.SaveService.QueueSave(new ModLocalData(mod)); + + return changes; + } + internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable? newModTags, IEnumerable? newLocalTags) { if (newModTags == null && newLocalTags == null) diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 39dd20e4..0cebcf81 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,5 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; using Penumbra.Services; namespace Penumbra.Mods; @@ -28,4 +30,85 @@ public readonly struct ModMeta(Mod mod) : ISavable jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); } + + public static ModDataChangeType Load(ModDataEditor editor, ModCreator creator, Mod mod) + { + var metaFile = editor.SaveService.FileNames.ModMetaPath(mod); + if (!File.Exists(metaFile)) + { + Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); + return ModDataChangeType.Deletion; + } + + try + { + var text = File.ReadAllText(metaFile); + var json = JObject.Parse(text); + + var newFileVersion = json[nameof(FileVersion)]?.Value() ?? 0; + + // Empty name gets checked after loading and is not allowed. + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + + var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; + var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; + var newImage = json[nameof(Mod.Image)]?.Value() ?? string.Empty; + var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; + var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; + var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); + + ModDataChangeType changes = 0; + if (mod.Name != newName) + { + changes |= ModDataChangeType.Name; + mod.Name = newName; + } + + if (mod.Author != newAuthor) + { + changes |= ModDataChangeType.Author; + mod.Author = newAuthor; + } + + if (mod.Description != newDescription) + { + changes |= ModDataChangeType.Description; + mod.Description = newDescription; + } + + if (mod.Image != newImage) + { + changes |= ModDataChangeType.Image; + mod.Image = newImage; + } + + if (mod.Version != newVersion) + { + changes |= ModDataChangeType.Version; + mod.Version = newVersion; + } + + if (mod.Website != newWebsite) + { + changes |= ModDataChangeType.Website; + mod.Website = newWebsite; + } + + if (newFileVersion != FileVersion) + if (ModMigration.Migrate(creator, editor.SaveService, mod, json, ref newFileVersion)) + { + changes |= ModDataChangeType.Migration; + editor.SaveService.ImmediateSave(new ModMeta(mod)); + } + + changes |= ModLocalData.UpdateTags(mod, modTags, null); + + return changes; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not load mod meta for {metaFile}:\n{e}"); + return ModDataChangeType.Deletion; + } + } } From ec3ec7db4e686318c9d7c2ee7a3ded8cc6357d4f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 17 Jan 2025 19:55:02 +0100 Subject: [PATCH 546/865] update schema organization anf change some things. --- Penumbra.sln | 32 ++ schemas/container.json | 486 --------------------- schemas/default_mod.json | 20 +- schemas/group.json | 189 +------- schemas/local_mod_data-v3.json | 32 ++ schemas/{meta-v3.json => mod_meta-v3.json} | 17 +- schemas/structs/container.json | 34 ++ schemas/structs/group_combining.json | 31 ++ schemas/structs/group_imc.json | 50 +++ schemas/structs/group_multi.json | 32 ++ schemas/structs/group_single.json | 22 + schemas/structs/manipulation.json | 95 ++++ schemas/structs/meta_atch.json | 67 +++ schemas/structs/meta_enums.json | 57 +++ schemas/structs/meta_eqdp.json | 30 ++ schemas/structs/meta_eqp.json | 20 + schemas/structs/meta_est.json | 28 ++ schemas/structs/meta_geqp.json | 40 ++ schemas/structs/meta_gmp.json | 59 +++ schemas/structs/meta_imc.json | 87 ++++ schemas/structs/meta_rsp.json | 20 + schemas/structs/option.json | 24 + 22 files changed, 796 insertions(+), 676 deletions(-) delete mode 100644 schemas/container.json create mode 100644 schemas/local_mod_data-v3.json rename schemas/{meta-v3.json => mod_meta-v3.json} (79%) create mode 100644 schemas/structs/container.json create mode 100644 schemas/structs/group_combining.json create mode 100644 schemas/structs/group_imc.json create mode 100644 schemas/structs/group_multi.json create mode 100644 schemas/structs/group_single.json create mode 100644 schemas/structs/manipulation.json create mode 100644 schemas/structs/meta_atch.json create mode 100644 schemas/structs/meta_enums.json create mode 100644 schemas/structs/meta_eqdp.json create mode 100644 schemas/structs/meta_eqp.json create mode 100644 schemas/structs/meta_est.json create mode 100644 schemas/structs/meta_geqp.json create mode 100644 schemas/structs/meta_gmp.json create mode 100644 schemas/structs/meta_imc.json create mode 100644 schemas/structs/meta_rsp.json create mode 100644 schemas/structs/option.json diff --git a/Penumbra.sln b/Penumbra.sln index 94a04ef3..c0b38118 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -24,6 +24,34 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.String", "Penumbra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Penumbra.CrashHandler\Penumbra.CrashHandler.csproj", "{EE834491-A98F-4395-BE0D-6861AE5AD953}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}" + ProjectSection(SolutionItems) = preProject + schemas\files\default_mod.json = schemas\files\default_mod.json + schemas\files\group.json = schemas\files\group.json + schemas\files\local_mod_data-v3.json = schemas\files\local_mod_data-v3.json + schemas\files\mod_meta-v3.json = schemas\files\mod_meta-v3.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}" + ProjectSection(SolutionItems) = preProject + schemas\structs\container.json = schemas\structs\container.json + schemas\structs\group_combining.json = schemas\structs\group_combining.json + schemas\structs\group_imc.json = schemas\structs\group_imc.json + schemas\structs\group_multi.json = schemas\structs\group_multi.json + schemas\structs\group_single.json = schemas\structs\group_single.json + schemas\structs\manipulation.json = schemas\structs\manipulation.json + schemas\structs\meta_atch.json = schemas\structs\meta_atch.json + schemas\structs\meta_enums.json = schemas\structs\meta_enums.json + schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json + schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json + schemas\structs\meta_est.json = schemas\structs\meta_est.json + schemas\structs\meta_geqp.json = schemas\structs\meta_geqp.json + schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json + schemas\structs\meta_imc.json = schemas\structs\meta_imc.json + schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json + schemas\structs\option.json = schemas\structs\option.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +86,10 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BFEA7504-1210-4F79-A7FE-BF03B6567E33} = {F89C9EAE-25C8-43BE-8108-5921E5A93502} + {B03F276A-0572-4F62-AF86-EF62F6B80463} = {BFEA7504-1210-4F79-A7FE-BF03B6567E33} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} EndGlobalSection diff --git a/schemas/container.json b/schemas/container.json deleted file mode 100644 index 5798f46c..00000000 --- a/schemas/container.json +++ /dev/null @@ -1,486 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", - "type": "object", - "properties": { - "Name": { - "description": "Name of the container/option/sub-mod.", - "type": ["string", "null"] - }, - "Files": { - "description": "File redirections in this container. Keys are game paths, values are relative file paths.", - "type": "object", - "patternProperties": { - "^[^/\\\\][^\\\\]*$": { - "type": "string", - "pattern": "^[^/\\\\][^/]*$" - } - }, - "additionalProperties": false - }, - "FileSwaps": { - "description": "File swaps in this container. Keys are original game paths, values are actual game paths.", - "type": "object", - "patternProperties": { - "^[^/\\\\][^\\\\]*$": { - "type": "string", - "pattern": "^[^/\\\\][^/]*$" - } - }, - "additionalProperties": false - }, - "Manipulations": { - "type": "array", - "items": { - "$ref": "#/$defs/Manipulation" - } - } - }, - "$defs": { - "Manipulation": { - "type": "object", - "properties": { - "Type": { - "enum": ["Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch"] - }, - "Manipulation": { - "type": ["object", "null"] - } - }, - "required": ["Type", "Manipulation"], - "oneOf": [ - { - "properties": { - "Type": { - "const": "Unknown" - }, - "Manipulation": { - "type": "null" - } - } - }, { - "properties": { - "Type": { - "const": "Imc" - }, - "Manipulation": { - "$ref": "#/$defs/ImcManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Eqdp" - }, - "Manipulation": { - "$ref": "#/$defs/EqdpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Eqp" - }, - "Manipulation": { - "$ref": "#/$defs/EqpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Est" - }, - "Manipulation": { - "$ref": "#/$defs/EstManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Gmp" - }, - "Manipulation": { - "$ref": "#/$defs/GmpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Rsp" - }, - "Manipulation": { - "$ref": "#/$defs/RspManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "GlobalEqp" - }, - "Manipulation": { - "$ref": "#/$defs/GlobalEqpManipulation" - } - } - }, { - "properties": { - "Type": { - "const": "Atch" - }, - "Manipulation": { - "$ref": "#/$defs/AtchManipulation" - } - } - } - ], - "additionalProperties": false - }, - "ImcManipulation": { - "type": "object", - "properties": { - "Entry": { - "$ref": "#/$defs/ImcEntry" - }, - "Valid": { - "type": "boolean" - } - }, - "required": [ - "Entry" - ], - "allOf": [ - { - "$ref": "#/$defs/ImcIdentifier" - } - ], - "unevaluatedProperties": false - }, - "ImcIdentifier": { - "type": "object", - "properties": { - "PrimaryId": { - "type": "integer" - }, - "SecondaryId": { - "type": "integer" - }, - "Variant": { - "type": "integer" - }, - "ObjectType": { - "$ref": "#/$defs/ObjectType" - }, - "EquipSlot": { - "$ref": "#/$defs/EquipSlot" - }, - "BodySlot": { - "$ref": "#/$defs/BodySlot" - } - }, - "required": [ - "PrimaryId", - "SecondaryId", - "Variant", - "ObjectType", - "EquipSlot", - "BodySlot" - ] - }, - "ImcEntry": { - "type": "object", - "properties": { - "AttributeAndSound": { - "type": "integer" - }, - "MaterialId": { - "type": "integer" - }, - "DecalId": { - "type": "integer" - }, - "VfxId": { - "type": "integer" - }, - "MaterialAnimationId": { - "type": "integer" - }, - "AttributeMask": { - "type": "integer" - }, - "SoundId": { - "type": "integer" - } - }, - "required": [ - "MaterialId", - "DecalId", - "VfxId", - "MaterialAnimationId", - "AttributeMask", - "SoundId" - ], - "additionalProperties": false - }, - "EqdpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "$ref": "#/$defs/EquipSlot" - }, - "ShiftedEntry": { - "type": "integer" - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "EqpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "$ref": "#/$defs/EquipSlot" - } - }, - "required": [ - "Entry", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "EstManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "integer" - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - }, - "Slot": { - "enum": ["Hair", "Face", "Body", "Head"] - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "SetId", - "Slot" - ], - "additionalProperties": false - }, - "GmpManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "object", - "properties": { - "Enabled": { - "type": "boolean" - }, - "Animated": { - "type": "boolean" - }, - "RotationA": { - "type": "number" - }, - "RotationB": { - "type": "number" - }, - "RotationC": { - "type": "number" - }, - "UnknownA": { - "type": "number" - }, - "UnknownB": { - "type": "number" - }, - "UnknownTotal": { - "type": "number" - }, - "Value": { - "type": "number" - } - }, - "required": [ - "Enabled", - "Animated", - "RotationA", - "RotationB", - "RotationC", - "UnknownA", - "UnknownB", - "UnknownTotal", - "Value" - ], - "additionalProperties": false - }, - "SetId": { - "$ref": "#/$defs/LaxInteger" - } - }, - "required": [ - "Entry", - "SetId" - ], - "additionalProperties": false - }, - "RspManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "number" - }, - "SubRace": { - "$ref": "#/$defs/SubRace" - }, - "Attribute": { - "$ref": "#/$defs/RspAttribute" - } - }, - "additionalProperties": false - }, - "GlobalEqpManipulation": { - "type": "object", - "properties": { - "Condition": { - "type": "integer" - }, - "Type": { - "enum": ["DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats"] - } - }, - "additionalProperties": false - }, - "AtchManipulation": { - "type": "object", - "properties": { - "Entry": { - "type": "object", - "properties": { - "Bone": { - "type": "string", - "maxLength": 34 - }, - "Scale": { - "type": "number" - }, - "OffsetX": { - "type": "number" - }, - "OffsetY": { - "type": "number" - }, - "OffsetZ": { - "type": "number" - }, - "RotationX": { - "type": "number" - }, - "RotationY": { - "type": "number" - }, - "RotationZ": { - "type": "number" - } - }, - "required": [ - "Bone", - "Scale", - "OffsetX", - "OffsetY", - "OffsetZ", - "RotationX", - "RotationY", - "RotationZ" - ], - "additionalProperties": false - }, - "Gender": { - "$ref": "#/$defs/Gender" - }, - "Race": { - "$ref": "#/$defs/ModelRace" - }, - "Type": { - "type": "string", - "minLength": 3, - "maxLength": 3 - }, - "Index": { - "type": "integer" - } - }, - "required": [ - "Entry", - "Gender", - "Race", - "Type", - "Index" - ], - "additionalProperties": false - }, - "LaxInteger": { - "oneOf": [ - { - "type": "integer" - }, { - "type": "string", - "pattern": "^\\d+$" - } - ] - }, - "EquipSlot": { - "enum": ["Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All"] - }, - "Gender": { - "enum": ["Unknown", "Male", "Female", "MaleNpc", "FemaleNpc"] - }, - "ModelRace": { - "enum": ["Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera"] - }, - "ObjectType": { - "enum": ["Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font"] - }, - "BodySlot": { - "enum": ["Unknown", "Hair", "Face", "Tail", "Body", "Zear"] - }, - "SubRace": { - "enum": ["Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena"] - }, - "RspAttribute": { - "enum": ["MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ"] - } - } -} diff --git a/schemas/default_mod.json b/schemas/default_mod.json index eecd74d0..8f50c5db 100644 --- a/schemas/default_mod.json +++ b/schemas/default_mod.json @@ -1,25 +1,19 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json", "allOf": [ { "type": "object", "properties": { "Version": { - "description": "???", - "type": ["integer", "null"] - }, - "Description": { - "description": "Description of the sub-mod.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] + "description": "Mod Container version, currently unused.", + "type": "integer", + "minimum": 0, + "maximum": 0 } } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" + }, + { + "$ref": "structs/container.json" } ] } diff --git a/schemas/group.json b/schemas/group.json index 0078e9f3..4c37b631 100644 --- a/schemas/group.json +++ b/schemas/group.json @@ -1,35 +1,35 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/group.json", "type": "object", "properties": { "Version": { - "description": "???", - "type": ["integer", "null"] + "description": "Mod Container version, currently unused.", + "type": "integer" }, "Name": { "description": "Name of the group.", - "type": "string" + "type": "string", + "minLength": 1 }, "Description": { "description": "Description of the group.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Image": { "description": "Relative path to a preview image for the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", - "type": ["string", "null"] + "type": ["string", "null" ] }, "Page": { "description": "TexTools page of the group. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", - "type": ["integer", "null"] + "type": "integer" }, "Priority": { "description": "Priority of the group. If several groups define conflicting files or manipulations, the highest priority wins.", - "type": ["integer", "null"] + "type": "integer" }, "Type": { "description": "Group type. Single groups have one and only one of their options active at any point. Multi groups can have zero, one or many of their options active. Combining groups have n options, 2^n containers, and will have one and only one container active depending on the selected options.", - "enum": ["Single", "Multi", "Imc", "Combining"] + "enum": [ "Single", "Multi", "Imc", "Combining" ] }, "DefaultSettings": { "description": "Default configuration for the group.", @@ -42,165 +42,16 @@ ], "oneOf": [ { - "properties": { - "Type": { - "const": "Single" - }, - "Options": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - ] - } - } - } - }, { - "properties": { - "Type": { - "const": "Multi" - }, - "Options": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Priority": { - "description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.", - "type": ["integer", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - } - }, { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - ] - } - } - } - }, { - "properties": { - "Type": { - "const": "Imc" - }, - "AllVariants": { - "type": "boolean" - }, - "OnlyAttributes": { - "type": "boolean" - }, - "Identifier": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcIdentifier" - }, - "DefaultEntry": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json#/$defs/ImcEntry" - }, - "Options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "description": "Name of the option.", - "type": "string" - }, - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - }, - "required": [ - "Name" - ], - "oneOf": [ - { - "properties": { - "AttributeMask": { - "type": "integer" - } - }, - "required": [ - "AttributeMask" - ] - }, { - "properties": { - "IsDisableSubMod": { - "const": true - } - }, - "required": [ - "IsDisableSubMod" - ] - } - ], - "unevaluatedProperties": false - } - } - } - }, { - "properties": { - "Type": { - "const": "Combining" - }, - "Options": { - "type": "array", - "items": { - "type": "object", - "properties": { - "Name": { - "description": "Name of the option.", - "type": "string" - }, - "Description": { - "description": "Description of the option.", - "type": ["string", "null"] - }, - "Image": { - "description": "Unused by Penumbra.", - "type": ["string", "null"] - } - }, - "required": [ - "Name" - ], - "additionalProperties": false - } - }, - "Containers": { - "type": "array", - "items": { - "$ref": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/container.json" - } - } - } + "$ref": "structs/group_combining.json" + }, + { + "$ref": "structs/group_imc.json" + }, + { + "$ref": "structs/group_multi.json" + }, + { + "$ref": "structs/group_single.json" } - ], - "unevaluatedProperties": false + ] } diff --git a/schemas/local_mod_data-v3.json b/schemas/local_mod_data-v3.json new file mode 100644 index 00000000..bf5d1311 --- /dev/null +++ b/schemas/local_mod_data-v3.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Local Penumbra Mod Data", + "description": "The locally stored data for an installed mod in Penumbra", + "type": "object", + "properties": { + "FileVersion": { + "description": "Major version of the local data schema used.", + "type": "integer", + "minimum": 3, + "maximum": 3 + }, + "ImportDate": { + "description": "The date and time of the installation of the mod as a Unix Epoch millisecond timestamp.", + "type": "integer" + }, + "LocalTags": { + "description": "User-defined local tags for the mod.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "uniqueItems": true + }, + "Favorite": { + "description": "Whether the mod is favourited by the user.", + "type": "boolean" + } + }, + "required": [ "FileVersion" ] +} diff --git a/schemas/meta-v3.json b/schemas/mod_meta-v3.json similarity index 79% rename from schemas/meta-v3.json rename to schemas/mod_meta-v3.json index 1a132264..a926b49e 100644 --- a/schemas/meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -1,6 +1,5 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/meta-v3.json", "title": "Penumbra Mod Metadata", "description": "Metadata of a Penumbra mod.", "type": "object", @@ -13,33 +12,35 @@ }, "Name": { "description": "Name of the mod.", - "type": "string" + "type": "string", + "minLength": 1 }, "Author": { "description": "Author of the mod.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Description": { "description": "Description of the mod. Can span multiple paragraphs.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Image": { "description": "Relative path to a preview image for the mod. Unused by Penumbra, present for round-trip import/export of TexTools-generated mods.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Version": { "description": "Version of the mod. Can be an arbitrary string.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "Website": { "description": "URL of the web page of the mod.", - "type": ["string", "null"] + "type": [ "string", "null" ] }, "ModTags": { "description": "Author-defined tags for the mod.", "type": "array", "items": { - "type": "string" + "type": "string", + "minLength": 1 }, "uniqueItems": true } diff --git a/schemas/structs/container.json b/schemas/structs/container.json new file mode 100644 index 00000000..74db4a23 --- /dev/null +++ b/schemas/structs/container.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Files": { + "description": "File redirections in this container. Keys are game paths, values are relative file paths.", + "type": [ "object", "null" ], + "patternProperties": { + "^[^/\\\\.:?][^\\\\?:]+$": { + "type": "string", + "pattern": "^[^/\\\\.:?][^?:]+$" + } + }, + "additionalProperties": false + }, + "FileSwaps": { + "description": "File swaps in this container. Keys are original game paths, values are actual game paths.", + "type": [ "object", "null" ], + "patternProperties": { + "^[^/\\\\.?:][^\\\\?:]+$": { + "type": "string", + "pattern": "^[^/\\\\.:?][^?:]+$" + } + }, + "additionalProperties": false + }, + "Manipulations": { + "type": [ "array", "null" ], + "items": { + "$ref": "manipulation.json" + } + } + } +} diff --git a/schemas/structs/group_combining.json b/schemas/structs/group_combining.json new file mode 100644 index 00000000..e42edcb8 --- /dev/null +++ b/schemas/structs/group_combining.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "Type": { + "const": "Combining" + }, + "Options": { + "type": "array", + "items": { + "$ref": "option.json" + } + }, + "Containers": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "container.json" + }, + { + "properties": { + "Name": { + "type": [ "string", "null" ] + } + } + } + ] + } + } + } +} diff --git a/schemas/structs/group_imc.json b/schemas/structs/group_imc.json new file mode 100644 index 00000000..48a04bd9 --- /dev/null +++ b/schemas/structs/group_imc.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "Type": { + "const": "Imc" + }, + "AllVariants": { + "type": "boolean" + }, + "OnlyAttributes": { + "type": "boolean" + }, + "Identifier": { + "$ref": "meta_imc.json#ImcIdentifier" + }, + "DefaultEntry": { + "$ref": "meta_imc.json#ImcEntry" + }, + "Options": { + "type": "array", + "items": { + "$ref": "option.json", + "oneOf": [ + { + "properties": { + "AttributeMask": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + } + }, + "required": [ + "AttributeMask" + ] + }, + { + "properties": { + "IsDisableSubMod": { + "const": true + } + }, + "required": [ + "IsDisableSubMod" + ] + } + ] + } + } + } +} diff --git a/schemas/structs/group_multi.json b/schemas/structs/group_multi.json new file mode 100644 index 00000000..ca7d4dfa --- /dev/null +++ b/schemas/structs/group_multi.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Type": { + "const": "Multi" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "option.json" + }, + { + "$ref": "container.json" + }, + { + "properties": { + "Priority": { + "type": "integer" + } + } + } + ] + } + } + } +} + + + diff --git a/schemas/structs/group_single.json b/schemas/structs/group_single.json new file mode 100644 index 00000000..24cda88d --- /dev/null +++ b/schemas/structs/group_single.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Type": { + "const": "Single" + }, + "Options": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "option.json" + }, + { + "$ref": "container.json" + } + ] + } + } + } +} diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json new file mode 100644 index 00000000..4a41dbe2 --- /dev/null +++ b/schemas/structs/manipulation.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Type": { + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch" ] + }, + "Manipulation": { + "type": "object" + } + }, + "required": [ "Type", "Manipulation" ], + "oneOf": [ + { + "properties": { + "Type": { + "const": "Imc" + }, + "Manipulation": { + "$ref": "meta_imc.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Eqdp" + }, + "Manipulation": { + "$ref": "meta_eqdp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Eqp" + }, + "Manipulation": { + "$ref": "meta_eqp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Est" + }, + "Manipulation": { + "$ref": "meta_est.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Gmp" + }, + "Manipulation": { + "$ref": "meta_gmp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Rsp" + }, + "Manipulation": { + "$ref": "meta_rsp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "GlobalEqp" + }, + "Manipulation": { + "$ref": "meta_geqp.json" + } + } + }, + { + "properties": { + "Type": { + "const": "Atch" + }, + "Manipulation": { + "$ref": "meta_atch.json" + } + } + } + ] +} diff --git a/schemas/structs/meta_atch.json b/schemas/structs/meta_atch.json new file mode 100644 index 00000000..3c9cbef5 --- /dev/null +++ b/schemas/structs/meta_atch.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Bone": { + "type": "string", + "maxLength": 34 + }, + "Scale": { + "type": "number" + }, + "OffsetX": { + "type": "number" + }, + "OffsetY": { + "type": "number" + }, + "OffsetZ": { + "type": "number" + }, + "RotationX": { + "type": "number" + }, + "RotationY": { + "type": "number" + }, + "RotationZ": { + "type": "number" + } + }, + "required": [ + "Bone", + "Scale", + "OffsetX", + "OffsetY", + "OffsetZ", + "RotationX", + "RotationY", + "RotationZ" + ] + }, + "Gender": { + "$ref": "meta_enums.json#Gender" + }, + "Race": { + "$ref": "meta_enums.json#ModelRace" + }, + "Type": { + "type": "string", + "minLength": 1, + "maxLength": 4 + }, + "Index": { + "$ref": "meta_enums.json#U16" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "Type", + "Index" + ] +} diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json new file mode 100644 index 00000000..747da849 --- /dev/null +++ b/schemas/structs/meta_enums.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "EquipSlot": { + "$anchor": "EquipSlot", + "enum": [ "Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All" ] + }, + "Gender": { + "$anchor": "Gender", + "enum": [ "Unknown", "Male", "Female", "MaleNpc", "FemaleNpc" ] + }, + "ModelRace": { + "$anchor": "ModelRace", + "enum": [ "Unknown", "Midlander", "Highlander", "Elezen", "Lalafell", "Miqote", "Roegadyn", "AuRa", "Hrothgar", "Viera" ] + }, + "ObjectType": { + "$anchor": "ObjectType", + "enum": [ "Unknown", "Vfx", "DemiHuman", "Accessory", "World", "Housing", "Monster", "Icon", "LoadingScreen", "Map", "Interface", "Equipment", "Character", "Weapon", "Font" ] + }, + "BodySlot": { + "$anchor": "BodySlot", + "enum": [ "Unknown", "Hair", "Face", "Tail", "Body", "Zear" ] + }, + "SubRace": { + "$anchor": "SubRace", + "enum": [ "Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena" ] + }, + "U8": { + "$anchor": "U8", + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + { + "type": "string", + "pattern": "^0*(1[0-9][0-9]|[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$" + } + ] + }, + "U16": { + "$anchor": "U16", + "oneOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + { + "type": "string", + "pattern": "^0*([1-5][0-9]{4}|[0-9]{0,4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" + } + ] + } + } +} diff --git a/schemas/structs/meta_eqdp.json b/schemas/structs/meta_eqdp.json new file mode 100644 index 00000000..f27606b9 --- /dev/null +++ b/schemas/structs/meta_eqdp.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "Gender": { + "$ref": "meta_enums.json#Gender" + }, + "Race": { + "$ref": "meta_enums.json#ModelRace" + }, + "SetId": { + "$ref": "meta_enums.json#U16" + }, + "Slot": { + "$ref": "meta_enums.json#EquipSlot" + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ] +} diff --git a/schemas/structs/meta_eqp.json b/schemas/structs/meta_eqp.json new file mode 100644 index 00000000..c829d7a7 --- /dev/null +++ b/schemas/structs/meta_eqp.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "integer" + }, + "SetId": { + "$ref": "meta_enums.json#U16" + }, + "Slot": { + "$ref": "meta_enums.json#EquipSlot" + } + }, + "required": [ + "Entry", + "SetId", + "Slot" + ] +} diff --git a/schemas/structs/meta_est.json b/schemas/structs/meta_est.json new file mode 100644 index 00000000..22bce12b --- /dev/null +++ b/schemas/structs/meta_est.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "$ref": "meta_enums.json#U16" + }, + "Gender": { + "$ref": "meta_enums.json#Gender" + }, + "Race": { + "$ref": "meta_enums.json#ModelRace" + }, + "SetId": { + "$ref": "meta_enums.json#U16" + }, + "Slot": { + "enum": [ "Hair", "Face", "Body", "Head" ] + } + }, + "required": [ + "Entry", + "Gender", + "Race", + "SetId", + "Slot" + ] +} diff --git a/schemas/structs/meta_geqp.json b/schemas/structs/meta_geqp.json new file mode 100644 index 00000000..3d4908f9 --- /dev/null +++ b/schemas/structs/meta_geqp.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Condition": { + "$ref": "meta_enums.json#U16" + }, + "Type": { + "enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + } + }, + "required": [ "Type" ], + "oneOf": [ + { + "properties": { + "Type": { + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + }, + "Condition": { + "const": 0 + } + } + }, + { + "properties": { + "Type": { + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + } + } + }, + { + "properties": { + "Type": { + "const": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL" ] + }, + "Condition": {} + } + } + ] +} diff --git a/schemas/structs/meta_gmp.json b/schemas/structs/meta_gmp.json new file mode 100644 index 00000000..bf1fb1df --- /dev/null +++ b/schemas/structs/meta_gmp.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "Animated": { + "type": "boolean" + }, + "RotationA": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "RotationB": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "RotationC": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "UnknownA": { + "type": "integer", + "minimum": 0, + "maximum": 15 + }, + "UnknownB": { + "type": "integer", + "minimum": 0, + "maximum": 15 + } + }, + "required": [ + "Enabled", + "Animated", + "RotationA", + "RotationB", + "RotationC", + "UnknownA", + "UnknownB" + ], + "additionalProperties": false + }, + "SetId": { + "$ref": "meta_enums.json#U16" + } + }, + "required": [ + "Entry", + "SetId" + ] +} diff --git a/schemas/structs/meta_imc.json b/schemas/structs/meta_imc.json new file mode 100644 index 00000000..aa9a4fca --- /dev/null +++ b/schemas/structs/meta_imc.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "$ref": "#ImcEntry" + } + }, + "required": [ + "Entry" + ], + "allOf": [ + { + "$ref": "#ImcIdentifier" + } + ], + "$defs": { + "ImcIdentifier": { + "type": "object", + "properties": { + "PrimaryId": { + "$ref": "meta_enums.json#U16" + }, + "SecondaryId": { + "$ref": "meta_enums.json#U16" + }, + "Variant": { + "$ref": "meta_enums.json#U8" + }, + "ObjectType": { + "$ref": "meta_enums.json#ObjectType" + }, + "EquipSlot": { + "$ref": "meta_enums.json#EquipSlot" + }, + "BodySlot": { + "$ref": "meta_enums.json#BodySlot" + } + }, + "$anchor": "ImcIdentifier", + "required": [ + "PrimaryId", + "SecondaryId", + "Variant", + "ObjectType", + "EquipSlot", + "BodySlot" + ] + }, + "ImcEntry": { + "type": "object", + "properties": { + "MaterialId": { + "$ref": "meta_enums.json#U8" + }, + "DecalId": { + "$ref": "meta_enums.json#U8" + }, + "VfxId": { + "$ref": "meta_enums.json#U8" + }, + "MaterialAnimationId": { + "$ref": "meta_enums.json#U8" + }, + "AttributeMask": { + "type": "integer", + "minimum": 0, + "maximum": 1023 + }, + "SoundId": { + "type": "integer", + "minimum": 0, + "maximum": 63 + } + }, + "$anchor": "ImcEntry", + "required": [ + "MaterialId", + "DecalId", + "VfxId", + "MaterialAnimationId", + "AttributeMask", + "SoundId" + ] + } + } +} diff --git a/schemas/structs/meta_rsp.json b/schemas/structs/meta_rsp.json new file mode 100644 index 00000000..3354281b --- /dev/null +++ b/schemas/structs/meta_rsp.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "number" + }, + "SubRace": { + "$ref": "meta_enums.json#SubRace" + }, + "Attribute": { + "enum": [ "MaleMinSize", "MaleMaxSize", "MaleMinTail", "MaleMaxTail", "FemaleMinSize", "FemaleMaxSize", "FemaleMinTail", "FemaleMaxTail", "BustMinX", "BustMinY", "BustMinZ", "BustMaxX", "BustMaxY", "BustMaxZ" ] + } + }, + "required": [ + "Entry", + "SubRace", + "Attribute" + ] +} diff --git a/schemas/structs/option.json b/schemas/structs/option.json new file mode 100644 index 00000000..c45ccfdb --- /dev/null +++ b/schemas/structs/option.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Name": { + "description": "Name of the option.", + "type": "string", + "minLength": 1 + }, + "Description": { + "description": "Description of the option.", + "type": [ "string", "null" ] + }, + "Priority": { + "description": "Priority of the option. If several enabled options within the group define conflicting files or manipulations, the highest priority wins.", + "type": "integer" + }, + "Image": { + "description": "Unused by Penumbra.", + "type": [ "string", "null" ] + } + }, + "required": [ "Name" ] +} From b62563d72131c2119370b3d25677457291c15b96 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 17 Jan 2025 20:06:21 +0100 Subject: [PATCH 547/865] Remove $id from shpk_devkit schema --- schemas/shpk_devkit.json | 1 - 1 file changed, 1 deletion(-) diff --git a/schemas/shpk_devkit.json b/schemas/shpk_devkit.json index cd18ab81..f03fbb05 100644 --- a/schemas/shpk_devkit.json +++ b/schemas/shpk_devkit.json @@ -1,6 +1,5 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/xivdev/Penumbra/master/schemas/shpk_devkit.json", "type": "object", "properties": { "ShaderKeys": { From 4f0428832cadfe61c4c49dd7dcbfdeacde5332bd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:13:09 +0100 Subject: [PATCH 548/865] Fix solution file for schemas. --- Penumbra.sln | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index c0b38118..e864fbee 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -26,10 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.CrashHandler", "Pe EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Schemas", "Schemas", "{BFEA7504-1210-4F79-A7FE-BF03B6567E33}" ProjectSection(SolutionItems) = preProject - schemas\files\default_mod.json = schemas\files\default_mod.json - schemas\files\group.json = schemas\files\group.json - schemas\files\local_mod_data-v3.json = schemas\files\local_mod_data-v3.json - schemas\files\mod_meta-v3.json = schemas\files\mod_meta-v3.json + schemas\default_mod.json = schemas\default_mod.json + schemas\group.json = schemas\group.json + schemas\local_mod_data-v3.json = schemas\local_mod_data-v3.json + schemas\mod_meta-v3.json = schemas\mod_meta-v3.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F276A-0572-4F62-AF86-EF62F6B80463}" From 7b517390b6c619a27de6698ada6627b80ae21c75 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:30:03 +0100 Subject: [PATCH 549/865] Fix temporary settings causing collection saves. --- Penumbra/Collections/Manager/CollectionEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 437d4e0b..f4902fda 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -194,7 +194,8 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void InvokeChange(ModCollection changedCollection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); + if (type is not ModSettingChange.TemporarySetting) + saveService.QueueSave(new ModCollectionSave(modStorage, changedCollection)); communicator.ModSettingChanged.Invoke(changedCollection, type, mod, oldValue, groupIdx, false); if (type is not ModSettingChange.TemporarySetting) RecurseInheritors(changedCollection, type, mod, oldValue, groupIdx); From 8779f4b6893b6d472c71fb63d0f31ef947140424 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 15:36:05 +0100 Subject: [PATCH 550/865] Add new cutscene ENPC tracking hooks. --- Penumbra.GameData | 2 +- Penumbra/Interop/GameState.cs | 3 +- Penumbra/Interop/Hooks/HookSettings.cs | 2 + .../Objects/ConstructCutsceneCharacter.cs | 70 +++++++++++++++++++ Penumbra/Interop/Hooks/Objects/EnableDraw.cs | 2 +- .../Interop/Hooks/Objects/SetupPlayerNpc.cs | 55 +++++++++++++++ .../Interop/PathResolving/CutsceneService.cs | 29 +++++--- 7 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs create mode 100644 Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs diff --git a/Penumbra.GameData b/Penumbra.GameData index c5250722..5bac66e5 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c525072299d5febd2bb638ab229060b0073ba6a6 +Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2 diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index f80ef696..497be508 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -11,7 +11,8 @@ public class GameState : IService { #region Last Game Object - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + public readonly ThreadLocal CharacterAssociated = new(() => false); public nint LastGameObject => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index b95e5789..5a856764 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -76,6 +76,8 @@ public class HookOverrides public bool CreateCharacterBase; public bool EnableDraw; public bool WeaponReload; + public bool SetupPlayerNpc; + public bool ConstructCutsceneCharacter; } public struct PostProcessingHooks diff --git a/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs b/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs new file mode 100644 index 00000000..5fa3de32 --- /dev/null +++ b/Penumbra/Interop/Hooks/Objects/ConstructCutsceneCharacter.cs @@ -0,0 +1,70 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.Interop.Hooks.Objects; + +public sealed unsafe class ConstructCutsceneCharacter : EventWrapperPtr, IHookService +{ + private readonly GameState _gameState; + private readonly ObjectManager _objects; + + public enum Priority + { + /// + CutsceneService = 0, + } + + public ConstructCutsceneCharacter(GameState gameState, HookManager hooks, ObjectManager objects) + : base("ConstructCutsceneCharacter") + { + _gameState = gameState; + _objects = objects; + _task = hooks.CreateHook(Name, Sigs.ConstructCutsceneCharacter, Detour, !HookOverrides.Instance.Objects.ConstructCutsceneCharacter); + } + + private readonly Task> _task; + + public delegate int Delegate(SetupPlayerNpc.SchedulerStruct* scheduler); + + public int Detour(SetupPlayerNpc.SchedulerStruct* scheduler) + { + // This is the function that actually creates the new game object + // and fills it into the object table at a free index etc. + var ret = _task.Result.Original(scheduler); + // Check for the copy state from SetupPlayerNpc. + if (_gameState.CharacterAssociated.Value) + { + // If the newly created character exists, invoke the event. + var character = _objects[ret + (int)ScreenActor.CutsceneStart].AsCharacter; + if (character != null) + { + Invoke(character); + Penumbra.Log.Verbose( + $"[{Name}] Created indirect copy of player character at 0x{(nint)character}, index {character->ObjectIndex}."); + } + _gameState.CharacterAssociated.Value = false; + } + + return ret; + } + + public IntPtr Address + => _task.Result.Address; + + public void Enable() + => _task.Result.Enable(); + + public void Disable() + => _task.Result.Disable(); + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; +} diff --git a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs index 68bb28af..979cb87c 100644 --- a/Penumbra/Interop/Hooks/Objects/EnableDraw.cs +++ b/Penumbra/Interop/Hooks/Objects/EnableDraw.cs @@ -26,7 +26,7 @@ public sealed unsafe class EnableDraw : IHookService private void Detour(GameObject* gameObject) { _state.QueueGameObject(gameObject); - Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X}."); + Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint)gameObject:X} at {gameObject->ObjectIndex}."); _task.Result.Original.Invoke(gameObject); _state.DequeueGameObject(); } diff --git a/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs b/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs new file mode 100644 index 00000000..8f1226c3 --- /dev/null +++ b/Penumbra/Interop/Hooks/Objects/SetupPlayerNpc.cs @@ -0,0 +1,55 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using OtterGui.Services; +using Penumbra.GameData; + +namespace Penumbra.Interop.Hooks.Objects; + +public sealed unsafe class SetupPlayerNpc : FastHook +{ + private readonly GameState _gameState; + + public SetupPlayerNpc(GameState gameState, HookManager hooks) + { + _gameState = gameState; + Task = hooks.CreateHook("SetupPlayerNPC", Sigs.SetupPlayerNpc, Detour, + !HookOverrides.Instance.Objects.SetupPlayerNpc); + } + + public delegate SchedulerStruct* Delegate(byte* npcType, nint unk, NpcSetupData* setupData); + + public SchedulerStruct* Detour(byte* npcType, nint unk, NpcSetupData* setupData) + { + // This function actually seems to generate all NPC. + + // If an ENPC is being created, check the creation parameters. + // If CopyPlayerCustomize is true, the event NPC gets a timeline that copies its customize and glasses from the local player. + // Keep track of this, so we can associate the actor to be created for this with the player character, see ConstructCutsceneCharacter. + if (setupData->CopyPlayerCustomize && npcType != null && *npcType is 8) + _gameState.CharacterAssociated.Value = true; + + var ret = Task.Result.Original.Invoke(npcType, unk, setupData); + Penumbra.Log.Excessive( + $"[Setup Player NPC] Invoked for type {*npcType} with 0x{unk:X} and Copy Player Customize: {setupData->CopyPlayerCustomize}."); + return ret; + } + + [StructLayout(LayoutKind.Explicit)] + public struct NpcSetupData + { + [FieldOffset(0x0B)] + private byte _copyPlayerCustomize; + + public bool CopyPlayerCustomize + { + get => _copyPlayerCustomize != 0; + set => _copyPlayerCustomize = value ? (byte)1 : (byte)0; + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct SchedulerStruct + { + public static Character* GetCharacter(SchedulerStruct* s) + => ((delegate* unmanaged**)s)[0][19](s); + } +} diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 8e32dd76..6be19c46 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -15,10 +15,11 @@ public sealed class CutsceneService : IRequiredService, IDisposable public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx; - private readonly ObjectManager _objects; - private readonly CopyCharacter _copyCharacter; - private readonly CharacterDestructor _characterDestructor; - private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); + private readonly ObjectManager _objects; + private readonly CopyCharacter _copyCharacter; + private readonly CharacterDestructor _characterDestructor; + private readonly ConstructCutsceneCharacter _constructCutsceneCharacter; + private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); public IEnumerable> Actors => Enumerable.Range(CutsceneStartIdx, CutsceneSlots) @@ -26,13 +27,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable .Select(i => KeyValuePair.Create(i, this[i] ?? _objects.GetDalamudObject(i)!)); public unsafe CutsceneService(ObjectManager objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor, - IClientState clientState) + ConstructCutsceneCharacter constructCutsceneCharacter, IClientState clientState) { - _objects = objects; - _copyCharacter = copyCharacter; - _characterDestructor = characterDestructor; + _objects = objects; + _copyCharacter = copyCharacter; + _characterDestructor = characterDestructor; + _constructCutsceneCharacter = constructCutsceneCharacter; _copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService); _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService); + _constructCutsceneCharacter.Subscribe(OnSetupPlayerNpc, ConstructCutsceneCharacter.Priority.CutsceneService); if (clientState.IsGPosing) RecoverGPoseActors(); } @@ -87,6 +90,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable { _copyCharacter.Unsubscribe(OnCharacterCopy); _characterDestructor.Unsubscribe(OnCharacterDestructor); + _constructCutsceneCharacter.Unsubscribe(OnSetupPlayerNpc); } private unsafe void OnCharacterDestructor(Character* character) @@ -124,6 +128,15 @@ public sealed class CutsceneService : IRequiredService, IDisposable _copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1); } + private unsafe void OnSetupPlayerNpc(Character* npc) + { + if (npc == null || npc->ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) + return; + + var idx = npc->GameObject.ObjectIndex - CutsceneStartIdx; + _copiedCharacters[idx] = 0; + } + /// Try to recover GPose actors on reloads into a running game. /// This is not 100% accurate due to world IDs, minions etc., but will be mostly sane. private void RecoverGPoseActors() From 0c8571fba92058b8281efcea0cdb2583104bf7ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 17:14:13 +0100 Subject: [PATCH 551/865] Reduce and pad IMC allocations and log allocations. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/ImcCache.cs | 1 - Penumbra/Collections/ModCollectionIdentity.cs | 6 +++--- Penumbra/Interop/GameState.cs | 4 ++-- Penumbra/Meta/Files/ImcFile.cs | 15 ++++++++++----- Penumbra/Meta/Files/MetaBaseFile.cs | 15 +++++++++++++-- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 5bac66e5..ebeea67c 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 5bac66e5ad73e57919aff7f8b046606b75e191a2 +Subproject commit ebeea67c17f6bf4ce7e635041b2138e835d31262 diff --git a/Penumbra/Collections/Cache/ImcCache.cs b/Penumbra/Collections/Cache/ImcCache.cs index 0f610d90..461ffccc 100644 --- a/Penumbra/Collections/Cache/ImcCache.cs +++ b/Penumbra/Collections/Cache/ImcCache.cs @@ -51,7 +51,6 @@ public sealed class ImcCache(MetaFileManager manager, ModCollection collection) if (!_imcFiles.TryGetValue(path, out var pair)) pair = (new ImcFile(Manager, identifier), []); - if (!Apply(pair.Item1, identifier, entry)) return; diff --git a/Penumbra/Collections/ModCollectionIdentity.cs b/Penumbra/Collections/ModCollectionIdentity.cs index c7f60005..bd2d47c4 100644 --- a/Penumbra/Collections/ModCollectionIdentity.cs +++ b/Penumbra/Collections/ModCollectionIdentity.cs @@ -10,9 +10,9 @@ public struct ModCollectionIdentity(Guid id, LocalCollectionId localId) public static readonly ModCollectionIdentity Empty = new(Guid.Empty, LocalCollectionId.Zero, EmptyCollectionName, 0); - public string Name { get; set; } - public Guid Id { get; } = id; - public LocalCollectionId LocalId { get; } = localId; + public string Name { get; set; } = string.Empty; + public Guid Id { get; } = id; + public LocalCollectionId LocalId { get; } = localId; /// The index of the collection is set and kept up-to-date by the CollectionManager. public int Index { get; internal set; } diff --git a/Penumbra/Interop/GameState.cs b/Penumbra/Interop/GameState.cs index 497be508..95cef468 100644 --- a/Penumbra/Interop/GameState.cs +++ b/Penumbra/Interop/GameState.cs @@ -11,8 +11,8 @@ public class GameState : IService { #region Last Game Object - private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); - public readonly ThreadLocal CharacterAssociated = new(() => false); + private readonly ThreadLocal> _lastGameObject = new(() => new Queue()); + public readonly ThreadLocal CharacterAssociated = new(() => false); public nint LastGameObject => _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 01ef3f16..de022f4c 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,3 +1,4 @@ +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; @@ -192,22 +193,26 @@ public unsafe class ImcFile : MetaBaseFile public void Replace(ResourceHandle* resource) { var (data, length) = resource->GetData(); - if (length == ActualLength) + var actualLength = ActualLength; + if (length >= actualLength) { - MemoryUtility.MemCpyUnchecked((byte*)data, Data, ActualLength); + MemoryUtility.MemCpyUnchecked((byte*)data, Data, actualLength); + MemoryUtility.MemSet((byte*)data + actualLength, 0, length - actualLength); return; } - var newData = Manager.XivAllocator.Allocate(ActualLength, 8); + var paddedLength = actualLength.PadToMultiple(128); + var newData = Manager.XivAllocator.Allocate(paddedLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength); + MemoryUtility.MemCpyUnchecked(newData, Data, actualLength); + MemoryUtility.MemSet((byte*)data + actualLength, 0, paddedLength - actualLength); Manager.XivAllocator.Release((void*)data, length); - resource->SetData((nint)newData, ActualLength); + resource->SetData((nint)newData, paddedLength); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 5bc36068..0cb34ab3 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -28,11 +28,16 @@ public unsafe interface IFileAllocator public sealed class MarshalAllocator : IFileAllocator { public unsafe T* Allocate(int length, int alignment = 1) where T : unmanaged - => (T*)Marshal.AllocHGlobal(length * sizeof(T)); + { + var ret = (T*)Marshal.AllocHGlobal(length * sizeof(T)); + Penumbra.Log.Verbose($"Allocating {length * sizeof(T)} bytes via Marshal Allocator to 0x{(nint)ret:X}."); + return ret; + } public unsafe void Release(ref T* pointer, int length) where T : unmanaged { Marshal.FreeHGlobal((nint)pointer); + Penumbra.Log.Verbose($"Freeing {length * sizeof(T)} bytes from 0x{(nint)pointer:X} via Marshal Allocator."); pointer = null; } } @@ -53,11 +58,17 @@ public sealed unsafe class XivFileAllocator : IFileAllocator, IService => ((delegate* unmanaged)_getFileSpaceAddress)(); public T* Allocate(int length, int alignment = 1) where T : unmanaged - => (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + { + var ret = (T*)GetFileSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + Penumbra.Log.Verbose($"Allocating {length * sizeof(T)} bytes via FFXIV File Allocator to 0x{(nint)ret:X}."); + return ret; + } public void Release(ref T* pointer, int length) where T : unmanaged { + IMemorySpace.Free(pointer, (ulong)(length * sizeof(T))); + Penumbra.Log.Verbose($"Freeing {length * sizeof(T)} bytes from 0x{(nint)pointer:X} via FFXIV File Allocator."); pointer = null; } } From 9ca0145a7f59e002b805a1c6060e05df67d7a7ac Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 20 Jan 2025 16:48:19 +0000 Subject: [PATCH 552/865] [CI] Updating repo.json for testing_1.3.3.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 2e3dd8bc..7e37f77c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.5", + "TestingAssemblyVersion": "1.3.3.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 39c73af2382228a0b5f867baf5be44979136deee Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 20 Jan 2025 20:33:28 +0100 Subject: [PATCH 553/865] Fix stupid. --- Penumbra/Meta/Files/ImcFile.cs | 14 ++++++++------ Penumbra/Meta/Files/MetaBaseFile.cs | 18 ++++++++++++++++++ Penumbra/Meta/MetaFileManager.cs | 26 ++++++++++++++------------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index de022f4c..c6e4ec94 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -197,22 +197,24 @@ public unsafe class ImcFile : MetaBaseFile if (length >= actualLength) { MemoryUtility.MemCpyUnchecked((byte*)data, Data, actualLength); - MemoryUtility.MemSet((byte*)data + actualLength, 0, length - actualLength); + if (length > actualLength) + MemoryUtility.MemSet((byte*)(data + actualLength), 0, length - actualLength); return; } var paddedLength = actualLength.PadToMultiple(128); - var newData = Manager.XivAllocator.Allocate(paddedLength, 8); + var newData = Manager.XivFileAllocator.Allocate(paddedLength, 8); if (newData == null) { Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - + MemoryUtility.MemCpyUnchecked(newData, Data, actualLength); - MemoryUtility.MemSet((byte*)data + actualLength, 0, paddedLength - actualLength); - - Manager.XivAllocator.Release((void*)data, length); + if (paddedLength > actualLength) + MemoryUtility.MemSet(newData + actualLength, 0, paddedLength - actualLength); + + Manager.XivFileAllocator.Release((void*)data, length); resource->SetData((nint)newData, paddedLength); } } diff --git a/Penumbra/Meta/Files/MetaBaseFile.cs b/Penumbra/Meta/Files/MetaBaseFile.cs index 0cb34ab3..d04e1bdf 100644 --- a/Penumbra/Meta/Files/MetaBaseFile.cs +++ b/Penumbra/Meta/Files/MetaBaseFile.cs @@ -73,6 +73,24 @@ public sealed unsafe class XivFileAllocator : IFileAllocator, IService } } +public sealed unsafe class XivDefaultAllocator : IFileAllocator, IService +{ + public T* Allocate(int length, int alignment = 1) where T : unmanaged + { + var ret = (T*)IMemorySpace.GetDefaultSpace()->Malloc((ulong)(length * sizeof(T)), (ulong)alignment); + Penumbra.Log.Verbose($"Allocating {length * sizeof(T)} bytes via FFXIV Default Allocator to 0x{(nint)ret:X}."); + return ret; + } + + public void Release(ref T* pointer, int length) where T : unmanaged + { + + IMemorySpace.Free(pointer, (ulong)(length * sizeof(T))); + Penumbra.Log.Verbose($"Freeing {length * sizeof(T)} bytes from 0x{(nint)pointer:X} via FFXIV Default Allocator."); + pointer = null; + } +} + public unsafe class MetaBaseFile(MetaFileManager manager, IFileAllocator alloc, MetaIndex idx) : IDisposable { protected readonly MetaFileManager Manager = manager; diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 5250273b..6130a48f 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -28,24 +28,26 @@ public class MetaFileManager : IService internal readonly ImcChecker ImcChecker; internal readonly AtchManager AtchManager; internal readonly IFileAllocator MarshalAllocator = new MarshalAllocator(); - internal readonly IFileAllocator XivAllocator; + internal readonly IFileAllocator XivFileAllocator; + internal readonly IFileAllocator XivDefaultAllocator; public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, IDataManager gameData, ActiveCollectionData activeCollections, Configuration config, ValidityChecker validityChecker, ObjectIdentification identifier, FileCompactor compactor, IGameInteropProvider interop, AtchManager atchManager) { - CharacterUtility = characterUtility; - ResidentResources = residentResources; - GameData = gameData; - ActiveCollections = activeCollections; - Config = config; - ValidityChecker = validityChecker; - Identifier = identifier; - Compactor = compactor; - AtchManager = atchManager; - ImcChecker = new ImcChecker(this); - XivAllocator = new XivFileAllocator(interop); + CharacterUtility = characterUtility; + ResidentResources = residentResources; + GameData = gameData; + ActiveCollections = activeCollections; + Config = config; + ValidityChecker = validityChecker; + Identifier = identifier; + Compactor = compactor; + AtchManager = atchManager; + ImcChecker = new ImcChecker(this); + XivFileAllocator = new XivFileAllocator(interop); + XivDefaultAllocator = new XivDefaultAllocator(); interop.InitializeFromAttributes(this); } From 737e74582bbc2a85ebbed6dfbc771adf881392fe Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 20 Jan 2025 19:36:05 +0000 Subject: [PATCH 554/865] [CI] Updating repo.json for testing_1.3.3.7 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7e37f77c..3e258788 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.6", + "TestingAssemblyVersion": "1.3.3.7", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.7/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2afd6b966e4c79c7c11bbcd9a2b2c8c074a91495 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jan 2025 17:36:01 +0100 Subject: [PATCH 555/865] Add debug logging facilities. --- OtterGui | 2 +- Penumbra.String | 2 +- Penumbra/DebugConfiguration.cs | 6 ++++ Penumbra/Meta/Files/ImcFile.cs | 29 +++++++++++++++++-- .../UI/Tabs/Debug/DebugConfigurationDrawer.cs | 14 +++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 1 + 6 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 Penumbra/DebugConfiguration.cs create mode 100644 Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs diff --git a/OtterGui b/OtterGui index 055f1695..3c1260c9 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 055f169572223fd1b59389549c88b4c861c94608 +Subproject commit 3c1260c9833303c2d33d12d6f77dc2b1afea3f34 diff --git a/Penumbra.String b/Penumbra.String index b9003b97..0bc2b0f6 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit b9003b97da2d1191fa203a4d66956bc54c21db2a +Subproject commit 0bc2b0f66eee1a02c9575b2bb30f27ce166f8632 diff --git a/Penumbra/DebugConfiguration.cs b/Penumbra/DebugConfiguration.cs new file mode 100644 index 00000000..76987df8 --- /dev/null +++ b/Penumbra/DebugConfiguration.cs @@ -0,0 +1,6 @@ +namespace Penumbra; + +public class DebugConfiguration +{ + public static bool WriteImcBytesToLog = false; +} diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index c6e4ec94..23339cfc 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,3 +1,4 @@ +using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -194,11 +195,28 @@ public unsafe class ImcFile : MetaBaseFile { var (data, length) = resource->GetData(); var actualLength = ActualLength; + + if (DebugConfiguration.WriteImcBytesToLog) + { + Penumbra.Log.Information($"Default IMC file -> Modified IMC File for {Path}:"); + Penumbra.Log.Information(new Span((void*)data, length).WriteHexBytes()); + Penumbra.Log.Information(new Span(Data, actualLength).WriteHexBytes()); + Penumbra.Log.Information(new Span(Data, actualLength).WriteHexByteDiff(new Span((void*)data, length))); + } + if (length >= actualLength) { MemoryUtility.MemCpyUnchecked((byte*)data, Data, actualLength); if (length > actualLength) MemoryUtility.MemSet((byte*)(data + actualLength), 0, length - actualLength); + if (DebugConfiguration.WriteImcBytesToLog) + { + Penumbra.Log.Information( + $"Copied {actualLength} bytes from local IMC file into {length} available bytes.{(length > actualLength ? $" Filled remaining {length - actualLength} bytes with 0." : string.Empty)}"); + Penumbra.Log.Information("Result IMC Resource Data:"); + Penumbra.Log.Information(new Span((void*)data, length).WriteHexBytes()); + } + return; } @@ -209,11 +227,18 @@ public unsafe class ImcFile : MetaBaseFile Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed."); return; } - + MemoryUtility.MemCpyUnchecked(newData, Data, actualLength); if (paddedLength > actualLength) MemoryUtility.MemSet(newData + actualLength, 0, paddedLength - actualLength); - + if (DebugConfiguration.WriteImcBytesToLog) + { + Penumbra.Log.Information( + $"Allocated {paddedLength} bytes for IMC file, copied {actualLength} bytes from local IMC file. {(length > actualLength ? $" Filled remaining {length - actualLength} bytes with 0." : string.Empty)}"); + Penumbra.Log.Information("Result IMC Resource Data:"); + Penumbra.Log.Information(new Span(newData, paddedLength).WriteHexBytes()); + } + Manager.XivFileAllocator.Release((void*)data, length); resource->SetData((nint)newData, paddedLength); } diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs new file mode 100644 index 00000000..34aafbea --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -0,0 +1,14 @@ +using OtterGui.Text; + +namespace Penumbra.UI.Tabs.Debug; + +public static class DebugConfigurationDrawer +{ + public static void Draw() + { + if (!ImUtf8.CollapsingHeaderId("Debug Logging Options")) + return; + + ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 77eeb3d7..ad4824c3 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -181,6 +181,7 @@ public class DebugTab : Window, ITab, IUiService DrawDebugTabGeneral(); _crashHandlerPanel.Draw(); + DebugConfigurationDrawer.Draw(); _diagnostics.DrawDiagnostics(); DrawPerformanceTab(); DrawPathResolverDebug(); From dcc435477738d79a3c4301bf862e89ffef705bb7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jan 2025 17:36:13 +0100 Subject: [PATCH 556/865] Fix clipping height in changed items tab. --- Penumbra/UI/Tabs/ChangedItemsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 256b0d79..5bac7d35 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -36,7 +36,7 @@ public class ChangedItemsTab( if (!child) return; - var height = ImGui.GetFrameHeight() + 2 * ImGui.GetStyle().CellPadding.Y; + var height = ImGui.GetFrameHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; var skips = ImGuiClip.GetNecessarySkips(height); using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); if (!list) From dcab443b2fb221104b471c4358bdf8bc8da8bc54 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 22 Jan 2025 16:38:33 +0000 Subject: [PATCH 557/865] [CI] Updating repo.json for testing_1.3.3.8 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3e258788..e0cf2a70 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.7", + "TestingAssemblyVersion": "1.3.3.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.8/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 40168d7daf418c5743558bb2906ae52456e9d7e7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Jan 2025 23:14:09 +0100 Subject: [PATCH 558/865] Fix issue with IPC adding mods before character utility is ready in rare cases. --- Penumbra/Api/IpcProviders.cs | 20 +++++++++++++--- .../Cache/CollectionCacheManager.cs | 6 ++--- .../Communication/CharacterUtilityFinished.cs | 23 +++++++++++++++++++ Penumbra/Interop/Services/CharacterUtility.cs | 21 +++++++++-------- Penumbra/Mods/Editor/ModMetaEditor.cs | 5 ++++ 5 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 Penumbra/Communication/CharacterUtilityFinished.cs diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index f6948832..fc97290f 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -2,6 +2,8 @@ using Dalamud.Plugin; using OtterGui.Services; using Penumbra.Api.Api; using Penumbra.Api.Helpers; +using Penumbra.Communication; +using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Api; @@ -9,11 +11,13 @@ public sealed class IpcProviders : IDisposable, IApiService { private readonly List _providers; - private readonly EventProvider _disposedProvider; - private readonly EventProvider _initializedProvider; + private readonly EventProvider _disposedProvider; + private readonly EventProvider _initializedProvider; + private readonly CharacterUtility _characterUtility; - public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api) + public IpcProviders(IDalamudPluginInterface pi, IPenumbraApi api, CharacterUtility characterUtility) { + _characterUtility = characterUtility; _disposedProvider = IpcSubscribers.Disposed.Provider(pi); _initializedProvider = IpcSubscribers.Initialized.Provider(pi); _providers = @@ -115,11 +119,21 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), ]; + if (_characterUtility.Ready) + _initializedProvider.Invoke(); + else + _characterUtility.LoadingFinished.Subscribe(OnCharacterUtilityReady, CharacterUtilityFinished.Priority.IpcProvider); + } + + private void OnCharacterUtilityReady() + { _initializedProvider.Invoke(); + _characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady); } public void Dispose() { + _characterUtility.LoadingFinished.Unsubscribe(OnCharacterUtilityReady); foreach (var provider in _providers) provider.Dispose(); _providers.Clear(); diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 27b969c2..c46759c7 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -71,7 +71,7 @@ public class CollectionCacheManager : IDisposable, IService _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionCacheManager); if (!MetaFileManager.CharacterUtility.Ready) - MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished.Subscribe(IncrementCounters, CharacterUtilityFinished.Priority.CollectionCacheManager); } public void Dispose() @@ -83,7 +83,7 @@ public class CollectionCacheManager : IDisposable, IService _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); - MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters); foreach (var collection in _storage) { @@ -298,7 +298,7 @@ public class CollectionCacheManager : IDisposable, IService { foreach (var collection in _storage.Where(c => c.HasCache)) collection.Counters.IncrementChange(); - MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters; + MetaFileManager.CharacterUtility.LoadingFinished.Unsubscribe(IncrementCounters); } private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool _) diff --git a/Penumbra/Communication/CharacterUtilityFinished.cs b/Penumbra/Communication/CharacterUtilityFinished.cs new file mode 100644 index 00000000..fbeeb8a7 --- /dev/null +++ b/Penumbra/Communication/CharacterUtilityFinished.cs @@ -0,0 +1,23 @@ +using OtterGui.Classes; +using Penumbra.Api; +using Penumbra.Interop.Services; + +namespace Penumbra.Communication; + +/// +/// Triggered when the Character Utility becomes ready. +/// +public sealed class CharacterUtilityFinished() : EventWrapper(nameof(CharacterUtilityFinished)) +{ + public enum Priority + { + /// + OnFinishedLoading = int.MaxValue, + + /// + IpcProvider = int.MinValue, + + /// + CollectionCacheManager = 0, + } +} diff --git a/Penumbra/Interop/Services/CharacterUtility.cs b/Penumbra/Interop/Services/CharacterUtility.cs index 1641e42d..0add9d46 100644 --- a/Penumbra/Interop/Services/CharacterUtility.cs +++ b/Penumbra/Interop/Services/CharacterUtility.cs @@ -1,6 +1,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using OtterGui.Services; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.Interop.Structs; @@ -26,14 +27,16 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService public CharacterUtilityData* Address => *_characterUtilityAddress; - public bool Ready { get; private set; } - public event Action LoadingFinished; - public nint DefaultHumanPbdResource { get; private set; } - public nint DefaultTransparentResource { get; private set; } - public nint DefaultDecalResource { get; private set; } - public nint DefaultSkinShpkResource { get; private set; } - public nint DefaultCharacterStockingsShpkResource { get; private set; } - public nint DefaultCharacterLegacyShpkResource { get; private set; } + public bool Ready { get; private set; } + + public readonly CharacterUtilityFinished LoadingFinished = new(); + + public nint DefaultHumanPbdResource { get; private set; } + public nint DefaultTransparentResource { get; private set; } + public nint DefaultDecalResource { get; private set; } + public nint DefaultSkinShpkResource { get; private set; } + public nint DefaultCharacterStockingsShpkResource { get; private set; } + public nint DefaultCharacterLegacyShpkResource { get; private set; } /// /// The relevant indices depend on which meta manipulations we allow for. @@ -61,7 +64,7 @@ public unsafe class CharacterUtility : IDisposable, IRequiredService .Select(idx => new MetaList(new InternalIndex(idx))) .ToArray(); _framework = framework; - LoadingFinished += () => Penumbra.Log.Debug("Loading of CharacterUtility finished."); + LoadingFinished.Subscribe(() => Penumbra.Log.Debug("Loading of CharacterUtility finished."), CharacterUtilityFinished.Priority.OnFinishedLoading); LoadDefaultResources(null!); if (!Ready) _framework.Update += LoadDefaultResources; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index c06af9c7..c5c8fb8b 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -70,6 +70,11 @@ public class ModMetaEditor( public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { + if (!metaFileManager.CharacterUtility.Ready) + { + Penumbra.Log.Warning("Trying to delete default meta values before CharacterUtility was ready, skipped."); + return false; + } var clone = dict.Clone(); dict.ClearForDefault(); From 55ce63383255b541d3644587a10db9c765b519b1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 23 Jan 2025 18:11:35 +0100 Subject: [PATCH 559/865] Try forcing IMC files to load synchronously for now. --- Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs | 8 ++++---- Penumbra/Interop/Processing/ImcFilePostProcessor.cs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index ad9c41e6..a74a3712 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -176,7 +176,7 @@ public unsafe class ResourceLoader : IDisposable, IService gamePath.Path.IsAscii); fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; - MtrlForceSync(fileDescriptor, ref isSync); + ForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; @@ -215,14 +215,14 @@ public unsafe class ResourceLoader : IDisposable, IService } } - /// Special handling for materials. - private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) + /// Special handling for materials and IMCs. + private static void ForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) { // Force isSync = true for Materials. I don't really understand why, // or where the difference even comes from. // Was called with True on my client and with false on other peoples clients, // which caused problems. - isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl; + isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl or ResourceType.Imc; } /// diff --git a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs index 513877d4..949baaa3 100644 --- a/Penumbra/Interop/Processing/ImcFilePostProcessor.cs +++ b/Penumbra/Interop/Processing/ImcFilePostProcessor.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.JobGauge.Types; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; From 0159eb3d8348b4d01e21d542aeeb28cf06201bc6 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 23 Jan 2025 17:13:52 +0000 Subject: [PATCH 560/865] [CI] Updating repo.json for testing_1.3.3.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e0cf2a70..0f61c85c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.8", + "TestingAssemblyVersion": "1.3.3.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From a3ddce0ef5aed4a0282039e79d46417b108c5716 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 24 Jan 2025 02:50:02 +0100 Subject: [PATCH 561/865] Add mechanism to handle completion of async res loads --- Penumbra.GameData | 2 +- Penumbra/Interop/Hooks/HookSettings.cs | 1 + .../Hooks/ResourceLoading/ResourceLoader.cs | 120 +++++++++++++++--- .../Hooks/ResourceLoading/ResourceService.cs | 78 ++++++++++-- .../Resources/ResourceHandleDestructor.cs | 5 +- .../Processing/FilePostProcessService.cs | 18 ++- Penumbra/Interop/Structs/ResourceHandle.cs | 15 ++- Penumbra/UI/ResourceWatcher/Record.cs | 29 ++++- .../UI/ResourceWatcher/ResourceWatcher.cs | 31 ++++- .../ResourceWatcher/ResourceWatcherTable.cs | 25 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 37 +++++- 11 files changed, 303 insertions(+), 58 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index ebeea67c..4a987167 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ebeea67c17f6bf4ce7e635041b2138e835d31262 +Subproject commit 4a987167b665184d4c05fc9863993981c35a1d19 diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 5a856764..bcff25d2 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -100,6 +100,7 @@ public class HookOverrides public bool DecRef; public bool GetResourceSync; public bool GetResourceAsync; + public bool UpdateResourceState; public bool CheckFileState; public bool TexResourceHandleOnLoad; public bool LoadMdlFileExtern; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index ad9c41e6..f9b8ff60 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -1,7 +1,9 @@ +using System.IO; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.PathResolving; using Penumbra.Interop.SafeHandles; using Penumbra.Interop.Structs; @@ -13,27 +15,38 @@ namespace Penumbra.Interop.Hooks.ResourceLoading; public unsafe class ResourceLoader : IDisposable, IService { - private readonly ResourceService _resources; - private readonly FileReadService _fileReadService; - private readonly RsfService _rsfService; - private readonly PapHandler _papHandler; - private readonly Configuration _config; + private readonly ResourceService _resources; + private readonly FileReadService _fileReadService; + private readonly RsfService _rsfService; + private readonly PapHandler _papHandler; + private readonly Configuration _config; + private readonly ResourceHandleDestructor _destructor; + + private readonly ConcurrentDictionary _ongoingLoads = []; private ResolveData _resolvedData = ResolveData.Invalid; public event Action? PapRequested; - public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner) + public IReadOnlyDictionary OngoingLoads + => _ongoingLoads; + + public ResourceLoader(ResourceService resources, FileReadService fileReadService, RsfService rsfService, Configuration config, PeSigScanner sigScanner, + ResourceHandleDestructor destructor) { _resources = resources; _fileReadService = fileReadService; - _rsfService = rsfService; + _rsfService = rsfService; _config = config; + _destructor = destructor; ResetResolvePath(); - _resources.ResourceRequested += ResourceHandler; - _resources.ResourceHandleIncRef += IncRefProtection; - _resources.ResourceHandleDecRef += DecRefProtection; - _fileReadService.ReadSqPack += ReadSqPackDetour; + _resources.ResourceRequested += ResourceHandler; + _resources.ResourceStateUpdating += ResourceStateUpdatingHandler; + _resources.ResourceStateUpdated += ResourceStateUpdatedHandler; + _resources.ResourceHandleIncRef += IncRefProtection; + _resources.ResourceHandleDecRef += DecRefProtection; + _fileReadService.ReadSqPack += ReadSqPackDetour; + _destructor.Subscribe(ResourceDestructorHandler, ResourceHandleDestructor.Priority.ResourceLoader); _papHandler = new PapHandler(sigScanner, PapResourceHandler); _papHandler.Enable(); @@ -109,12 +122,32 @@ public unsafe class ResourceLoader : IDisposable, IService /// public event FileLoadedDelegate? FileLoaded; + public delegate void ResourceCompleteDelegate(ResourceHandle* resource, CiByteString path, Utf8GamePath originalPath, + ReadOnlySpan additionalData, bool isAsync); + + /// + /// Event fired just before a resource finishes loading. + /// must be checked to know whether the load was successful or not. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// + public event ResourceCompleteDelegate? BeforeResourceComplete; + + /// + /// Event fired when a resource has finished loading. + /// must be checked to know whether the load was successful or not. + /// AdditionalData is either empty or the part of the path inside the leading pipes. + /// + public event ResourceCompleteDelegate? ResourceComplete; + public void Dispose() { - _resources.ResourceRequested -= ResourceHandler; - _resources.ResourceHandleIncRef -= IncRefProtection; - _resources.ResourceHandleDecRef -= DecRefProtection; - _fileReadService.ReadSqPack -= ReadSqPackDetour; + _resources.ResourceRequested -= ResourceHandler; + _resources.ResourceStateUpdating -= ResourceStateUpdatingHandler; + _resources.ResourceStateUpdated -= ResourceStateUpdatedHandler; + _resources.ResourceHandleIncRef -= IncRefProtection; + _resources.ResourceHandleDecRef -= DecRefProtection; + _fileReadService.ReadSqPack -= ReadSqPackDetour; + _destructor.Unsubscribe(ResourceDestructorHandler); _papHandler.Dispose(); } @@ -135,7 +168,8 @@ public unsafe class ResourceLoader : IDisposable, IService if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; } @@ -145,10 +179,57 @@ public unsafe class ResourceLoader : IDisposable, IService hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } + private void TrackResourceLoad(ResourceHandle* handle, Utf8GamePath original) + { + if (handle->UnkState == 2 && handle->LoadState >= LoadState.Success) + return; + + _ongoingLoads.TryAdd((nint)handle, original.Clone()); + } + + private void ResourceStateUpdatedHandler(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte, LoadState) previousState, ref uint returnValue) + { + if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState.Item1 == 2 && previousState.Item2 >= LoadState.Success) + return; + + if (!_ongoingLoads.TryRemove((nint)handle, out var asyncOriginal)) + asyncOriginal = Utf8GamePath.Empty; + + var path = handle->CsHandle.FileName; + if (!syncOriginal.IsEmpty && !asyncOriginal.IsEmpty && !syncOriginal.Equals(asyncOriginal)) + Penumbra.Log.Warning($"[ResourceLoader] Resource original paths inconsistency: 0x{(nint)handle:X}, of path {path}, sync original {syncOriginal}, async original {asyncOriginal}."); + var original = !asyncOriginal.IsEmpty ? asyncOriginal : syncOriginal; + + // Penumbra.Log.Information($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); + if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) + ResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); + else + ResourceComplete?.Invoke(handle, path.AsByteString(), original, [], !asyncOriginal.IsEmpty); + } + + private void ResourceStateUpdatingHandler(ResourceHandle* handle, Utf8GamePath syncOriginal) + { + if (handle->UnkState != 1 || handle->LoadState != LoadState.Success) + return; + + if (!_ongoingLoads.TryGetValue((nint)handle, out var asyncOriginal)) + asyncOriginal = Utf8GamePath.Empty; + + var path = handle->CsHandle.FileName; + var original = asyncOriginal.IsEmpty ? syncOriginal : asyncOriginal; + + // Penumbra.Log.Information($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); + if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) + BeforeResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); + else + BeforeResourceComplete?.Invoke(handle, path.AsByteString(), original, [], !asyncOriginal.IsEmpty); + } + private void ReadSqPackDetour(SeFileDescriptor* fileDescriptor, ref int priority, ref bool isSync, ref byte? returnValue) { if (fileDescriptor->ResourceHandle == null) @@ -265,6 +346,11 @@ public unsafe class ResourceLoader : IDisposable, IService returnValue = 1; } + private void ResourceDestructorHandler(ResourceHandle* handle) + { + _ongoingLoads.TryRemove((nint)handle, out _); + } + /// Compute the CRC32 hash for a given path together with potential resource parameters. private static int ComputeHash(CiByteString path, GetResourceParameters* pGetResParams) { diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 126505d1..238ed70f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -19,6 +19,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService private readonly PerformanceTracker _performance; private readonly ResourceManagerService _resourceManager; + private readonly ThreadLocal _currentGetResourcePath = new(() => Utf8GamePath.Empty); + public ResourceService(PerformanceTracker performance, ResourceManagerService resourceManager, IGameInteropProvider interop) { _performance = performance; @@ -34,6 +36,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService _getResourceSyncHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.GetResourceAsync) _getResourceAsyncHook.Enable(); + if (!HookOverrides.Instance.ResourceLoading.UpdateResourceState) + _updateResourceStateHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.IncRef) _incRefHook.Enable(); if (!HookOverrides.Instance.ResourceLoading.DecRef) @@ -54,8 +58,10 @@ public unsafe class ResourceService : IDisposable, IRequiredService { _getResourceSyncHook.Dispose(); _getResourceAsyncHook.Dispose(); + _updateResourceStateHook.Dispose(); _incRefHook.Dispose(); _decRefHook.Dispose(); + _currentGetResourcePath.Dispose(); } #region GetResource @@ -112,28 +118,84 @@ public unsafe class ResourceService : IDisposable, IRequiredService unk9); } + var original = gamePath; ResourceHandle* returnValue = null; - ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, gamePath, pGetResParams, ref isSync, + 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, pGetResParams, isUnk, unk8, unk9); + return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9, original); } /// Call the original GetResource function. public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, - GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0) - => sync - ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk8, unk9) - : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, - resourceParameters, unk, unk8, unk9); + GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0, Utf8GamePath original = default) + { + if (original.Path is null) // i. e. if original is default + Utf8GamePath.FromByteString(path, out original); + var previous = _currentGetResourcePath.Value; + try + { + _currentGetResourcePath.Value = original; + return sync + ? _getResourceSyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk8, unk9) + : _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path, + resourceParameters, unk, unk8, unk9); + } finally + { + _currentGetResourcePath.Value = previous; + } + } #endregion private delegate nint ResourceHandlePrototype(ResourceHandle* handle); + #region UpdateResourceState + + /// Invoked before a resource state is updated. + /// The resource handle. + /// The original game path of the resource, if loaded synchronously. + public delegate void ResourceStateUpdatingDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal); + + /// Invoked after a resource state is updated. + /// The resource handle. + /// 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); + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceStateUpdatingDelegate? ResourceStateUpdating; + + /// + /// + /// Subscribers should be exception-safe. + /// + public event ResourceStateUpdatedDelegate? ResourceStateUpdated; + + private delegate uint UpdateResourceStatePrototype(ResourceHandle* handle, byte offFileThread); + + [Signature(Sigs.UpdateResourceState, DetourName = nameof(UpdateResourceStateDetour))] + private readonly Hook _updateResourceStateHook = null!; + + private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread) + { + var previousState = (handle->UnkState, handle->LoadState); + 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); + return ret; + } + + #endregion + #region IncRef /// Invoked before a resource handle reference count is incremented. diff --git a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs index bdb11752..0e04029b 100644 --- a/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs +++ b/Penumbra/Interop/Hooks/Resources/ResourceHandleDestructor.cs @@ -14,9 +14,12 @@ public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr SubfileHelper, - /// + /// ShaderReplacementFixer, + /// + ResourceLoader, + /// ResourceWatcher, } diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index ecf78c69..a27f6d45 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -4,6 +4,7 @@ using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.String.Classes; namespace Penumbra.Interop.Processing; @@ -20,20 +21,23 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable public FilePostProcessService(ResourceLoader resourceLoader, ServiceManager services) { - _resourceLoader = resourceLoader; - _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.FileLoaded += OnFileLoaded; + _resourceLoader = resourceLoader; + _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); + _resourceLoader.BeforeResourceComplete += OnResourceComplete; } public void Dispose() { - _resourceLoader.FileLoaded -= OnFileLoaded; + _resourceLoader.BeforeResourceComplete -= OnResourceComplete; } - private void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool returnValue, bool custom, - ReadOnlySpan additionalData) + private void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + ReadOnlySpan additionalData, bool isAsync) { + if (resource->LoadState != LoadState.Success) + return; + if (_processors.TryGetValue(resource->FileType, out var processor)) - processor.PostProcess(resource, path, additionalData); + processor.PostProcess(resource, original.Path, additionalData); } } diff --git a/Penumbra/Interop/Structs/ResourceHandle.cs b/Penumbra/Interop/Structs/ResourceHandle.cs index 65550563..1558c035 100644 --- a/Penumbra/Interop/Structs/ResourceHandle.cs +++ b/Penumbra/Interop/Structs/ResourceHandle.cs @@ -24,10 +24,20 @@ public unsafe struct TextureResourceHandle public enum LoadState : byte { + Constructing = 0x00, + Constructed = 0x01, + Async2 = 0x02, + AsyncRequested = 0x03, + Async4 = 0x04, + AsyncLoading = 0x05, + Async6 = 0x06, Success = 0x07, - Async = 0x03, + Unknown8 = 0x08, Failure = 0x09, FailedSubResource = 0x0A, + FailureB = 0x0B, + FailureC = 0x0C, + FailureD = 0x0D, None = 0xFF, } @@ -74,6 +84,9 @@ public unsafe struct ResourceHandle [FieldOffset(0x58)] public int FileNameLength; + [FieldOffset(0xA8)] + public byte UnkState; + [FieldOffset(0xA9)] public LoadState LoadState; diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 7338e5a9..13a71656 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -10,10 +10,11 @@ namespace Penumbra.UI.ResourceWatcher; [Flags] public enum RecordType : byte { - Request = 0x01, - ResourceLoad = 0x02, - FileLoad = 0x04, - Destruction = 0x08, + Request = 0x01, + ResourceLoad = 0x02, + FileLoad = 0x04, + Destruction = 0x08, + ResourceComplete = 0x10, } internal unsafe struct Record @@ -141,4 +142,24 @@ internal unsafe struct Record LoadState = handle->LoadState, Crc64 = 0, }; + + public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath) + => new() + { + Time = DateTime.UtcNow, + Path = path.IsOwned ? path : path.Clone(), + OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), + Collection = null, + Handle = handle, + ResourceType = handle->FileType.ToFlag(), + Category = handle->Category.ToFlag(), + RefCount = handle->RefCount, + RecordType = RecordType.ResourceComplete, + Synchronously = false, + ReturnValue = OptionalBool.Null, + CustomLoad = OptionalBool.Null, + AssociatedGameObject = string.Empty, + LoadState = handle->LoadState, + Crc64 = 0, + }; } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 0f72efff..53d7e79d 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -47,9 +47,10 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _table = new ResourceWatcherTable(config.Ephemeral, _records); _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); - _loader.ResourceLoaded += OnResourceLoaded; - _loader.FileLoaded += OnFileLoaded; - _loader.PapRequested += OnPapRequested; + _loader.ResourceLoaded += OnResourceLoaded; + _loader.ResourceComplete += OnResourceComplete; + _loader.FileLoaded += OnFileLoaded; + _loader.PapRequested += OnPapRequested; UpdateFilter(_ephemeral.ResourceLoggingFilter, false); _newMaxEntries = _config.MaxResourceWatcherRecords; } @@ -73,9 +74,10 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _records.TrimExcess(); _resources.ResourceRequested -= OnResourceRequested; _destructor.Unsubscribe(OnResourceDestroyed); - _loader.ResourceLoaded -= OnResourceLoaded; - _loader.FileLoaded -= OnFileLoaded; - _loader.PapRequested -= OnPapRequested; + _loader.ResourceLoaded -= OnResourceLoaded; + _loader.ResourceComplete -= OnResourceComplete; + _loader.FileLoaded -= OnFileLoaded; + _loader.PapRequested -= OnPapRequested; } private void Clear() @@ -255,6 +257,23 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan _, bool isAsync) + { + if (!isAsync) + return; + + if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) + Penumbra.Log.Information( + $"[ResourceLoader] [DONE] [{resource->FileType}] Finished loading {match} into 0x{(ulong)resource:X}, state {resource->LoadState}."); + + if (!_ephemeral.EnableResourceWatcher) + return; + + var record = Record.CreateResourceComplete(path, resource, original); + if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) + _newRecords.Enqueue(record); + } + private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) { if (_ephemeral.EnableResourceLogging && FilterMatch(path, out var match)) diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 7ac3cb99..a58d74d1 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -124,11 +124,12 @@ internal sealed class ResourceWatcherTable : Table { ImGui.TextUnformatted(item.RecordType switch { - RecordType.Request => "REQ", - RecordType.ResourceLoad => "LOAD", - RecordType.FileLoad => "FILE", - RecordType.Destruction => "DEST", - _ => string.Empty, + RecordType.Request => "REQ", + RecordType.ResourceLoad => "LOAD", + RecordType.FileLoad => "FILE", + RecordType.Destruction => "DEST", + RecordType.ResourceComplete => "DONE", + _ => string.Empty, }); } } @@ -317,10 +318,10 @@ internal sealed class ResourceWatcherTable : Table { LoadState.None => FilterValue.HasFlag(LoadStateFlag.None), LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Success), - LoadState.Async => FilterValue.HasFlag(LoadStateFlag.Async), - LoadState.Failure => FilterValue.HasFlag(LoadStateFlag.Failed), LoadState.FailedSubResource => FilterValue.HasFlag(LoadStateFlag.FailedSub), - _ => FilterValue.HasFlag(LoadStateFlag.Unknown), + <= LoadState.Constructed => FilterValue.HasFlag(LoadStateFlag.Unknown), + < LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Async), + > LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Failed), }; public override void DrawColumn(Record item, int _) @@ -332,12 +333,12 @@ internal sealed class ResourceWatcherTable : Table { LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(), $"Successfully loaded ({(byte)item.LoadState})."), - LoadState.Async => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), - LoadState.Failure => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), - $"Failed to load ({(byte)item.LoadState})."), LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(), $"Dependencies failed to load ({(byte)item.LoadState})."), - _ => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Unknown state ({(byte)item.LoadState})."), + <= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), $"Not yet loaded ({(byte)item.LoadState})."), + < LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), $"Loading asynchronously ({(byte)item.LoadState})."), + > LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), + $"Failed to load ({(byte)item.LoadState})."), }; using (var font = ImRaii.PushFont(UiBuilder.IconFont)) { diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index ad4824c3..5dc203c2 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -80,6 +80,7 @@ public class DebugTab : Window, ITab, IUiService private readonly StainService _stains; private readonly GlobalVariablesDrawer _globalVariablesDrawer; private readonly ResourceManagerService _resourceManager; + private readonly ResourceLoader _resourceLoader; private readonly CollectionResolver _collectionResolver; private readonly DrawObjectState _drawObjectState; private readonly PathState _pathState; @@ -109,7 +110,7 @@ public class DebugTab : Window, ITab, IUiService public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, - ResourceManagerService resourceManager, CollectionResolver collectionResolver, + ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, @@ -133,6 +134,7 @@ public class DebugTab : Window, ITab, IUiService _actors = actors; _stains = stains; _resourceManager = resourceManager; + _resourceLoader = resourceLoader; _collectionResolver = collectionResolver; _drawObjectState = drawObjectState; _pathState = pathState; @@ -191,6 +193,7 @@ public class DebugTab : Window, ITab, IUiService DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); + DrawResourceLoader(); DrawResourceProblems(); _renderTargetDrawer.Draw(); _hookOverrides.Draw(); @@ -1099,6 +1102,38 @@ public class DebugTab : Window, ITab, IUiService } } + private unsafe void DrawResourceLoader() + { + if (!ImGui.CollapsingHeader("Resource Loader")) + return; + + var ongoingLoads = _resourceLoader.OngoingLoads; + var ongoingLoadCount = ongoingLoads.Count; + ImUtf8.Text($"Ongoing Loads: {ongoingLoadCount}"); + + if (ongoingLoadCount == 0) + return; + + using var table = ImUtf8.Table("ongoingLoadTable"u8, 3); + if (!table) + return; + + ImUtf8.TableSetupColumn("Resource Handle"u8, ImGuiTableColumnFlags.WidthStretch, 0.2f); + ImUtf8.TableSetupColumn("Actual Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImUtf8.TableSetupColumn("Original Path"u8, ImGuiTableColumnFlags.WidthStretch, 0.4f); + ImGui.TableHeadersRow(); + + foreach (var (handle, original) in ongoingLoads) + { + ImGui.TableNextColumn(); + ImUtf8.Text($"0x{handle:X}"); + ImGui.TableNextColumn(); + ImUtf8.Text(((ResourceHandle*)handle)->CsHandle.FileName); + ImGui.TableNextColumn(); + ImUtf8.Text(original.Path.Span); + } + } + /// Draw resources with unusual reference count. private unsafe void DrawResourceProblems() { From 9ab8985343591de70204d16a0abe1d97ffc3955a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Jan 2025 12:38:10 +0100 Subject: [PATCH 562/865] Debug logging. --- Penumbra/Meta/Files/ImcFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 23339cfc..0a0faf1e 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -198,7 +198,7 @@ public unsafe class ImcFile : MetaBaseFile if (DebugConfiguration.WriteImcBytesToLog) { - Penumbra.Log.Information($"Default IMC file -> Modified IMC File for {Path}:"); + Penumbra.Log.Information($"Default IMC file -> Modified IMC File for {Path}, current handle state {resource->LoadState}:"); Penumbra.Log.Information(new Span((void*)data, length).WriteHexBytes()); Penumbra.Log.Information(new Span(Data, actualLength).WriteHexBytes()); Penumbra.Log.Information(new Span(Data, actualLength).WriteHexByteDiff(new Span((void*)data, length))); From 30a957356af4e9b959a1280a015de1a3a34672f0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Jan 2025 13:54:19 +0100 Subject: [PATCH 563/865] Minor changes. --- .../Hooks/ResourceLoading/ResourceLoader.cs | 11 +++++------ .../Hooks/ResourceLoading/ResourceService.cs | 10 ++++------ .../Processing/FilePostProcessService.cs | 9 +++------ Penumbra/UI/ResourceWatcher/Record.cs | 17 +++++++++++++++-- Penumbra/UI/ResourceWatcher/ResourceWatcher.cs | 4 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 11 ++++------- 6 files changed, 33 insertions(+), 29 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index f9b8ff60..d5e41b56 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -1,4 +1,3 @@ -using System.IO; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -168,7 +167,7 @@ public unsafe class ResourceLoader : IDisposable, IService if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) { - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, original, parameters); TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, path, resolvedPath, data); return; @@ -179,7 +178,7 @@ public unsafe class ResourceLoader : IDisposable, IService hash = ComputeHash(resolvedPath.Value.InternalName, parameters); var oldPath = path; path = p; - returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, parameters, original: original); + returnValue = _resources.GetOriginalResource(sync, category, type, hash, path.Path, original, parameters); TrackResourceLoad(returnValue, original); ResourceLoaded?.Invoke(returnValue, oldPath, resolvedPath.Value, data); } @@ -194,7 +193,7 @@ public unsafe class ResourceLoader : IDisposable, IService private void ResourceStateUpdatedHandler(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte, LoadState) previousState, ref uint returnValue) { - if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState.Item1 == 2 && previousState.Item2 >= LoadState.Success) + if (handle->UnkState != 2 || handle->LoadState < LoadState.Success || previousState is { Item1: 2, Item2: >= LoadState.Success }) return; if (!_ongoingLoads.TryRemove((nint)handle, out var asyncOriginal)) @@ -205,7 +204,7 @@ public unsafe class ResourceLoader : IDisposable, IService Penumbra.Log.Warning($"[ResourceLoader] Resource original paths inconsistency: 0x{(nint)handle:X}, of path {path}, sync original {syncOriginal}, async original {asyncOriginal}."); var original = !asyncOriginal.IsEmpty ? asyncOriginal : syncOriginal; - // Penumbra.Log.Information($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); + Penumbra.Log.Excessive($"[ResourceLoader] Resource is complete: 0x{(nint)handle:X}, of path {path}, original {original}, state {previousState.Item1}:{previousState.Item2} -> {handle->UnkState}:{handle->LoadState}, sync: {asyncOriginal.IsEmpty}"); if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) ResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); else @@ -223,7 +222,7 @@ public unsafe class ResourceLoader : IDisposable, IService var path = handle->CsHandle.FileName; var original = asyncOriginal.IsEmpty ? syncOriginal : asyncOriginal; - // Penumbra.Log.Information($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); + Penumbra.Log.Excessive($"[ResourceLoader] Resource is about to be complete: 0x{(nint)handle:X}, of path {path}, original {original}"); if (PathDataHandler.Split(path.AsSpan(), out var actualPath, out var additionalData)) BeforeResourceComplete?.Invoke(handle, new CiByteString(actualPath), original, additionalData, !asyncOriginal.IsEmpty); else diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs index 238ed70f..e90b4575 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceService.cs @@ -125,15 +125,13 @@ public unsafe class ResourceService : IDisposable, IRequiredService if (returnValue != null) return returnValue; - return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, pGetResParams, isUnk, unk8, unk9, original); + 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, - GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0, Utf8GamePath original = default) + 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) { - if (original.Path is null) // i. e. if original is default - Utf8GamePath.FromByteString(path, out original); var previous = _currentGetResourcePath.Value; try { @@ -187,7 +185,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); diff --git a/Penumbra/Interop/Processing/FilePostProcessService.cs b/Penumbra/Interop/Processing/FilePostProcessService.cs index a27f6d45..71340178 100644 --- a/Penumbra/Interop/Processing/FilePostProcessService.cs +++ b/Penumbra/Interop/Processing/FilePostProcessService.cs @@ -23,20 +23,17 @@ public unsafe class FilePostProcessService : IRequiredService, IDisposable { _resourceLoader = resourceLoader; _processors = services.GetServicesImplementing().ToFrozenDictionary(s => s.Type, s => s); - _resourceLoader.BeforeResourceComplete += OnResourceComplete; + _resourceLoader.BeforeResourceComplete += OnBeforeResourceComplete; } public void Dispose() { - _resourceLoader.BeforeResourceComplete -= OnResourceComplete; + _resourceLoader.BeforeResourceComplete -= OnBeforeResourceComplete; } - private void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + private void OnBeforeResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan additionalData, bool isAsync) { - if (resource->LoadState != LoadState.Success) - return; - if (_processors.TryGetValue(resource->FileType, out var processor)) processor.PostProcess(resource, original.Path, additionalData); } diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 13a71656..8ab96f4b 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -143,11 +143,11 @@ internal unsafe struct Record Crc64 = 0, }; - public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath) + public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan additionalData) => new() { Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), + Path = CombinedPath(path, additionalData), OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), Collection = null, Handle = handle, @@ -162,4 +162,17 @@ internal unsafe struct Record LoadState = handle->LoadState, Crc64 = 0, }; + + private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan additionalData) + { + if (additionalData.Length is 0) + return path.IsOwned ? path : path.Clone(); + + fixed (byte* ptr = additionalData) + { + // If a path has additional data and is split, it is always in the form of |{additionalData}|{path}, + // so we can just read from the start of additional data - 1 and sum their length +2 for the pipes. + return new CiByteString(new ReadOnlySpan(ptr - 1, additionalData.Length + 2 + path.Length)).Clone(); + } + } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 53d7e79d..94bd4307 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -257,7 +257,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } - private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan _, bool isAsync) + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan additionalData, bool isAsync) { if (!isAsync) return; @@ -269,7 +269,7 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService if (!_ephemeral.EnableResourceWatcher) return; - var record = Record.CreateResourceComplete(path, resource, original); + var record = Record.CreateResourceComplete(path, resource, original, additionalData); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5dc203c2..8f76a54a 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1104,7 +1104,7 @@ public class DebugTab : Window, ITab, IUiService private unsafe void DrawResourceLoader() { - if (!ImGui.CollapsingHeader("Resource Loader")) + if (!ImUtf8.CollapsingHeader("Resource Loader"u8)) return; var ongoingLoads = _resourceLoader.OngoingLoads; @@ -1125,12 +1125,9 @@ public class DebugTab : Window, ITab, IUiService foreach (var (handle, original) in ongoingLoads) { - ImGui.TableNextColumn(); - ImUtf8.Text($"0x{handle:X}"); - ImGui.TableNextColumn(); - ImUtf8.Text(((ResourceHandle*)handle)->CsHandle.FileName); - ImGui.TableNextColumn(); - ImUtf8.Text(original.Path.Span); + ImUtf8.DrawTableColumn($"0x{handle:X}"); + ImUtf8.DrawTableColumn(((ResourceHandle*)handle)->CsHandle.FileName); + ImUtf8.DrawTableColumn(original.Path.Span); } } From 4d26a63944f5841198ac889dda08187d2863adbc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 25 Jan 2025 13:55:13 +0100 Subject: [PATCH 564/865] Test disabling MtrlForceSync. --- .../Interop/Hooks/ResourceLoading/ResourceLoader.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index d5e41b56..3f8cb23f 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -256,7 +256,6 @@ public unsafe class ResourceLoader : IDisposable, IService gamePath.Path.IsAscii); fileDescriptor->ResourceHandle->FileNameData = path.Path; fileDescriptor->ResourceHandle->FileNameLength = path.Length; - MtrlForceSync(fileDescriptor, ref isSync); returnValue = DefaultLoadResource(path, fileDescriptor, priority, isSync, data); // Return original resource handle path so that they can be loaded separately. fileDescriptor->ResourceHandle->FileNameData = gamePath.Path.Path; @@ -295,16 +294,6 @@ public unsafe class ResourceLoader : IDisposable, IService } } - /// Special handling for materials. - private static void MtrlForceSync(SeFileDescriptor* fileDescriptor, ref bool isSync) - { - // Force isSync = true for Materials. I don't really understand why, - // or where the difference even comes from. - // Was called with True on my client and with false on other peoples clients, - // which caused problems. - isSync |= fileDescriptor->ResourceHandle->FileType is ResourceType.Mtrl; - } - /// /// A resource with ref count 0 that gets incremented goes through GetResourceAsync again. /// This means, that if the path determined from that is different than the resources path, From ac64b4db24bcba33646dceb43e9aa304403135c8 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 25 Jan 2025 13:01:04 +0000 Subject: [PATCH 565/865] [CI] Updating repo.json for testing_1.3.3.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0f61c85c..85ba406a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.3.1", - "TestingAssemblyVersion": "1.3.3.9", + "TestingAssemblyVersion": "1.3.3.10", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b0a8b1baa5110d0aed944f462c7c13c7bcd2aac3 Mon Sep 17 00:00:00 2001 From: Theo <58579310+Theo-Asterio@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:11:46 -0800 Subject: [PATCH 566/865] Bone and Material Limit updates. Fix UI in Models tab to allow for more than 4 Materials per DT spec. --- Penumbra/Import/Models/Import/ModelImporter.cs | 10 +++++----- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index a141d754..5367e892 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -208,10 +208,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) if (index >= 0) return (ushort)index; - // If there's already 4 materials, we can't add any more. + // If there's already 10 materials, we can't add any more. // TODO: permit, with a warning to reduce, and validation in MdlTab. var count = _materials.Count; - if (count >= 4) + if (count >= 10) return 0; _materials.Add(materialName); @@ -234,10 +234,10 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) boneIndices.Add((ushort)boneIndex); } - if (boneIndices.Count > 64) - throw notifier.Exception("XIV does not support meshes weighted to a total of more than 64 bones."); + if (boneIndices.Count > 128) + throw notifier.Exception("XIV does not support meshes weighted to a total of more than 128 bones."); - var boneIndicesArray = new ushort[64]; + var boneIndicesArray = new ushort[128]; Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); var boneTableIndex = _boneTables.Count; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index de088736..bbf3dd00 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -16,7 +16,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const int MdlMaterialMaximum = 4; + private const int MdlMaterialMaximum = 10; private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; @@ -93,7 +93,7 @@ public partial class ModEditWindow tab.Mdl.ConvertV5ToV6(); _modelTab.SaveFile(); - } + } private void DrawImportExport(MdlTab tab, bool disabled) { @@ -427,7 +427,7 @@ public partial class ModEditWindow private static void DrawInvalidMaterialMarker() { - using (ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); ImGuiUtil.HoverTooltip( From 64748790cc877f613833a3de9f366e2e21168d9d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 30 Jan 2025 14:06:39 +0100 Subject: [PATCH 567/865] Make limits a bit cleaner. --- Penumbra/Import/Models/Import/ModelImporter.cs | 14 ++++++++------ Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 5367e892..502d060a 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -8,6 +8,9 @@ namespace Penumbra.Import.Models.Import; public partial class ModelImporter(ModelRoot model, IoNotifier notifier) { + public const int BoneLimit = 128; + public const int MaterialLimit = 10; + public static MdlFile Import(ModelRoot model, IoNotifier notifier) { var importer = new ModelImporter(model, notifier); @@ -208,10 +211,9 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) if (index >= 0) return (ushort)index; - // If there's already 10 materials, we can't add any more. // TODO: permit, with a warning to reduce, and validation in MdlTab. var count = _materials.Count; - if (count >= 10) + if (count >= MaterialLimit) return 0; _materials.Add(materialName); @@ -234,11 +236,11 @@ public partial class ModelImporter(ModelRoot model, IoNotifier notifier) boneIndices.Add((ushort)boneIndex); } - if (boneIndices.Count > 128) - throw notifier.Exception("XIV does not support meshes weighted to a total of more than 128 bones."); + if (boneIndices.Count > BoneLimit) + throw notifier.Exception($"XIV does not support meshes weighted to a total of more than {BoneLimit} bones."); - var boneIndicesArray = new ushort[128]; - Array.Copy(boneIndices.ToArray(), boneIndicesArray, boneIndices.Count); + var boneIndicesArray = new ushort[BoneLimit]; + boneIndices.CopyTo(boneIndicesArray); var boneTableIndex = _boneTables.Count; _boneTables.Add(new BoneTableStruct() diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index bbf3dd00..8fbe5a68 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -9,6 +9,7 @@ using OtterGui.Widgets; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; +using Penumbra.Import.Models.Import; using Penumbra.String.Classes; using Penumbra.UI.Classes; @@ -16,7 +17,7 @@ namespace Penumbra.UI.AdvancedWindow; public partial class ModEditWindow { - private const int MdlMaterialMaximum = 10; + private const int MdlMaterialMaximum = ModelImporter.MaterialLimit; private const string MdlImportDocumentation = @"https://github.com/xivdev/Penumbra/wiki/Model-IO#user-content-9b49d296-23ab-410a-845b-a3be769b71ea"; From 7022b37043c5f02d29d0078a8d086c374d30aa4a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Jan 2025 15:31:05 +0100 Subject: [PATCH 568/865] Add some improved Mod Setting API. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 68 +++++++++++++++--- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 + .../Api/IpcTester/ModSettingsIpcTester.cs | 72 +++++++++++++++---- 5 files changed, 120 insertions(+), 26 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index b4e716f8..35b25bef 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit b4e716f86d94cd4d98d8f58e580ed5f619ea87ae +Subproject commit 35b25bef92e9b0be96c44c150a3df89d848d2658 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index b78523d3..fe9bf366 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -65,6 +65,16 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance) + { + var ret = GetCurrentModSettingsWithTemp(collectionId, modDirectory, modName, ignoreInheritance, true, 0); + if (ret.Item2 is null) + return (ret.Item1, null); + + return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4)); + } + + public (PenumbraApiEc, (bool, int, Dictionary>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId, + string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return (PenumbraApiEc.ModMissing, null); @@ -72,17 +82,32 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (!_collectionManager.Storage.ById(collectionId, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - var settings = collection.Identity.Id == Guid.Empty - ? null - : ignoreInheritance - ? collection.GetOwnSettings(mod.Index) - : collection.GetInheritedSettings(mod.Index).Settings; - if (settings == null) + if (collection.Identity.Id == Guid.Empty) return (PenumbraApiEc.Success, null); - var (enabled, priority, dict) = settings.ConvertToShareable(mod); - return (PenumbraApiEc.Success, - (enabled, priority.Value, dict, collection.GetOwnSettings(mod.Index) is null)); + if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings) + return (PenumbraApiEc.Success, settings); + + return (PenumbraApiEc.Success, null); + } + + public (PenumbraApiEc, Dictionary>, bool, bool)>?) GetAllModSettings(Guid collectionId, + bool ignoreInheritance, bool ignoreTemporary, int key) + { + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return (PenumbraApiEc.CollectionMissing, null); + + if (collection.Identity.Id == Guid.Empty) + return (PenumbraApiEc.Success, []); + + var ret = new Dictionary>, bool, bool)>(_modManager.Count); + foreach (var mod in _modManager) + { + if (GetCurrentSettings(collection, mod, ignoreInheritance, ignoreTemporary, key) is { } settings) + ret[mod.Identifier] = settings; + } + + return (PenumbraApiEc.Success, ret); } public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) @@ -206,6 +231,31 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (bool, int, Dictionary>, bool, bool)? GetCurrentSettings(ModCollection collection, Mod mod, + bool ignoreInheritance, bool ignoreTemporary, int key) + { + var settings = collection.Settings.Settings[mod.Index]; + if (!ignoreTemporary && settings.TempSettings is { } tempSettings && (tempSettings.Lock <= 0 || tempSettings.Lock == key)) + { + if (!tempSettings.ForceInherit) + return (tempSettings.Enabled, tempSettings.Priority.Value, tempSettings.ConvertToShareable(mod).Settings, + false, true); + if (!ignoreInheritance && collection.GetActualSettings(mod.Index).Settings is { } actualSettingsTemp) + return (actualSettingsTemp.Enabled, actualSettingsTemp.Priority.Value, + actualSettingsTemp.ConvertToShareable(mod).Settings, true, true); + } + + if (settings.Settings is { } ownSettings) + return (ownSettings.Enabled, ownSettings.Priority.Value, ownSettings.ConvertToShareable(mod).Settings, false, + false); + if (!ignoreInheritance && collection.GetInheritedSettings(mod.Index).Settings is { } actualSettings) + return (actualSettings.Enabled, actualSettings.Priority.Value, + actualSettings.ConvertToShareable(mod).Settings, true, false); + + return null; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 05c47644..cfc9d470 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 5); + => (5, 6); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index fc97290f..9733f82e 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -57,6 +57,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings), + IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings), IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings), diff --git a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs index 23078576..c8eb8496 100644 --- a/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs +++ b/Penumbra/Api/IpcTester/ModSettingsIpcTester.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; @@ -22,16 +23,20 @@ public class ModSettingsIpcTester : IUiService, IDisposable private bool _lastSettingChangeInherited; private DateTimeOffset _lastSettingChange; - private string _settingsModDirectory = string.Empty; - private string _settingsModName = string.Empty; - private Guid? _settingsCollection; - private string _settingsCollectionName = string.Empty; - private bool _settingsIgnoreInheritance; - private bool _settingsInherit; - private bool _settingsEnabled; - private int _settingsPriority; - private IReadOnlyDictionary? _availableSettings; - private Dictionary>? _currentSettings; + private string _settingsModDirectory = string.Empty; + private string _settingsModName = string.Empty; + private Guid? _settingsCollection; + private string _settingsCollectionName = string.Empty; + private bool _settingsIgnoreInheritance; + private bool _settingsIgnoreTemporary; + private int _settingsKey; + private bool _settingsInherit; + private bool _settingsTemporary; + private bool _settingsEnabled; + private int _settingsPriority; + private IReadOnlyDictionary? _availableSettings; + private Dictionary>? _currentSettings; + private Dictionary>, bool, bool)>? _allSettings; public ModSettingsIpcTester(IDalamudPluginInterface pi) { @@ -54,7 +59,9 @@ public class ModSettingsIpcTester : IUiService, IDisposable ImGui.InputTextWithHint("##settingsDir", "Mod Directory Name...", ref _settingsModDirectory, 100); ImGui.InputTextWithHint("##settingsName", "Mod Name...", ref _settingsModName, 100); ImGuiUtil.GuidInput("##settingsCollection", "Collection...", string.Empty, ref _settingsCollection, ref _settingsCollectionName); - ImGui.Checkbox("Ignore Inheritance", ref _settingsIgnoreInheritance); + ImUtf8.Checkbox("Ignore Inheritance"u8, ref _settingsIgnoreInheritance); + ImUtf8.Checkbox("Ignore Temporary"u8, ref _settingsIgnoreTemporary); + ImUtf8.InputScalar("Key"u8, ref _settingsKey); var collection = _settingsCollection.GetValueOrDefault(Guid.Empty); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); @@ -83,10 +90,11 @@ public class ModSettingsIpcTester : IUiService, IDisposable _lastSettingsError = ret.Item1; if (ret.Item1 == PenumbraApiEc.Success) { - _settingsEnabled = ret.Item2?.Item1 ?? false; - _settingsInherit = ret.Item2?.Item4 ?? true; - _settingsPriority = ret.Item2?.Item2 ?? 0; - _currentSettings = ret.Item2?.Item3; + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? true; + _settingsTemporary = false; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; } else { @@ -94,6 +102,40 @@ public class ModSettingsIpcTester : IUiService, IDisposable } } + IpcTester.DrawIntro(GetCurrentModSettingsWithTemp.Label, "Get Current Settings With Temp"); + if (ImGui.Button("Get##CurrentTemp")) + { + var ret = new GetCurrentModSettingsWithTemp(_pi) + .Invoke(collection, _settingsModDirectory, _settingsModName, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey); + _lastSettingsError = ret.Item1; + if (ret.Item1 == PenumbraApiEc.Success) + { + _settingsEnabled = ret.Item2?.Item1 ?? false; + _settingsInherit = ret.Item2?.Item4 ?? true; + _settingsTemporary = ret.Item2?.Item5 ?? false; + _settingsPriority = ret.Item2?.Item2 ?? 0; + _currentSettings = ret.Item2?.Item3; + } + else + { + _currentSettings = null; + } + } + + IpcTester.DrawIntro(GetAllModSettings.Label, "Get All Mod Settings"); + if (ImGui.Button("Get##All")) + { + var ret = new GetAllModSettings(_pi).Invoke(collection, _settingsIgnoreInheritance, _settingsIgnoreTemporary, _settingsKey); + _lastSettingsError = ret.Item1; + _allSettings = ret.Item2; + } + + if (_allSettings != null) + { + ImGui.SameLine(); + ImUtf8.Text($"{_allSettings.Count} Mods"); + } + IpcTester.DrawIntro(TryInheritMod.Label, "Inherit Mod"); ImGui.Checkbox("##inherit", ref _settingsInherit); ImGui.SameLine(); From ec09a7eb0ee1aafc061306c5cd4e72b1316ae83b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 31 Jan 2025 18:46:17 +0100 Subject: [PATCH 569/865] Add initial cleaning functions, to be improved. --- .../Collections/Manager/CollectionStorage.cs | 12 ++- Penumbra/Services/CleanupService.cs | 74 +++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 31 +++++++- 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 Penumbra/Services/CleanupService.cs diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index e19acd35..de723729 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -194,12 +194,16 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer } /// Remove all settings for not currently-installed mods from the given collection. - public void CleanUnavailableSettings(ModCollection collection) + public int CleanUnavailableSettings(ModCollection collection) { - var any = collection.Settings.Unused.Count > 0; - ((Dictionary)collection.Settings.Unused).Clear(); - if (any) + var count = collection.Settings.Unused.Count; + if (count > 0) + { + ((Dictionary)collection.Settings.Unused).Clear(); _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + + return count; } /// Remove a specific setting for not currently-installed mods from the given collection. diff --git a/Penumbra/Services/CleanupService.cs b/Penumbra/Services/CleanupService.cs new file mode 100644 index 00000000..490c2407 --- /dev/null +++ b/Penumbra/Services/CleanupService.cs @@ -0,0 +1,74 @@ +using OtterGui.Services; +using Penumbra.Collections.Manager; +using Penumbra.Mods.Manager; + +namespace Penumbra.Services; + +public class CleanupService(SaveService saveService, ModManager mods, CollectionManager collections) : IService +{ + public void CleanUnusedLocalData() + { + var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet(); + foreach (var file in saveService.FileNames.LocalDataFiles.ToList()) + { + try + { + if (!file.Exists || usedFiles.Contains(file.FullName)) + continue; + + file.Delete(); + Penumbra.Log.Information($"[CleanupService] Deleted unused local data file {file.Name}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}"); + } + } + } + + public void CleanBackupFiles() + { + foreach (var file in mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories)) + { + try + { + if (!file.Exists) + continue; + + file.Delete(); + Penumbra.Log.Information($"[CleanupService] Deleted group backup file {file.FullName}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}"); + } + } + + foreach (var file in Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories)) + { + try + { + if (!File.Exists(file)) + continue; + + File.Delete(file); + Penumbra.Log.Information($"[CleanupService] Deleted config backup file {file}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}"); + } + } + } + + public void CleanupAllUnusedSettings() + { + foreach (var collection in collections.Storage) + { + var count = collections.Storage.CleanUnavailableSettings(collection); + if (count > 0) + Penumbra.Log.Information( + $"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}."); + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c7f66859..e847b291 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -49,6 +49,7 @@ public class SettingsTab : ITab, IUiService private readonly CrashHandlerService _crashService; private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; + private readonly CleanupService _cleanupService; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -60,7 +61,7 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService) { _pluginInterface = pluginInterface; _config = config; @@ -84,6 +85,7 @@ public class SettingsTab : ITab, IUiService _crashService = crashService; _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; + _cleanupService = cleanupService; } public void DrawHeader() @@ -789,9 +791,13 @@ public class SettingsTab : ITab, IUiService DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); + ImGui.Separator(); DrawReloadResourceButton(); DrawReloadFontsButton(); + ImGui.Separator(); + DrawCleanupButtons(); ImGui.NewLine(); + } private void DrawCrashHandler() @@ -982,6 +988,29 @@ public class SettingsTab : ITab, IUiService _fontReloader.Reload(); } + private void DrawCleanupButtons() + { + var enabled = _config.DeleteModModifier.IsActive(); + if (ImUtf8.ButtonEx("Clear Unused Local Mod Data Files"u8, + "Delete all local mod data files that do not correspond to currently installed mods."u8, default, !enabled)) + _cleanupService.CleanUnusedLocalData(); + if (!enabled) + ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + + if (ImUtf8.ButtonEx("Clear Backup Files"u8, + "Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8, + default, !enabled)) + _cleanupService.CleanBackupFiles(); + if (!enabled) + ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + + if (ImUtf8.ButtonEx("Clear All Unused Settings"u8, + "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default, !enabled)) + _cleanupService.CleanupAllUnusedSettings(); + if (!enabled) + ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to remove settings."); + } + /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. private void DrawWaitForPluginsReflection() { From 981c2bace4d32c404447a14c95e7129ff4212163 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 3 Feb 2025 00:20:22 +0100 Subject: [PATCH 570/865] Fix out-of-root path detection logic --- Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index f5659e7c..95627566 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -25,6 +25,8 @@ public class ResourceTreeFactory( PathState pathState, ModManager modManager) : IService { + private static readonly string ParentDirectoryPrefix = $"..{Path.DirectorySeparatorChar}"; + private TreeBuildCache CreateTreeBuildCache() => new(objects, gameData, actors); @@ -159,7 +161,7 @@ public class ResourceTreeFactory( if (onlyWithinPath != null) { var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName); - if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath))) + if (relPath == ".." || relPath.StartsWith(ParentDirectoryPrefix) || Path.IsPathRooted(relPath)) return false; } From f9b163e7c51a63bf01a4242f4d25cef7cef9c74c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Mon, 3 Feb 2025 00:51:13 +0100 Subject: [PATCH 571/865] Add explanations on why paths are redacted --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 9 +++++++++ .../ResourceTree/ResourceTreeFactory.cs | 13 +++++++----- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 20 +++++++++++++++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 4fa13e1f..60cc48de 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -16,6 +16,7 @@ public class ResourceNode : ICloneable public readonly nint ResourceHandle; public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; + public PathStatus FullPathStatus; public string? ModName; public readonly WeakReference Mod = new(null!); public string? ModRelativePath; @@ -61,6 +62,7 @@ public class ResourceNode : ICloneable ResourceHandle = other.ResourceHandle; PossibleGamePaths = other.PossibleGamePaths; FullPath = other.FullPath; + FullPathStatus = other.FullPathStatus; ModName = other.ModName; Mod = other.Mod; ModRelativePath = other.ModRelativePath; @@ -100,4 +102,11 @@ public class ResourceNode : ICloneable public UiData PrependName(string prefix) => Name == null ? this : this with { Name = prefix + Name }; } + + public enum PathStatus : byte + { + Valid, + NonExistent, + External, + } } diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index 95627566..cb8be184 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -147,25 +147,28 @@ public class ResourceTreeFactory( { foreach (var node in tree.FlatNodes) { - if (!ShallKeepPath(node.FullPath, onlyWithinPath)) + node.FullPathStatus = GetPathStatus(node.FullPath, onlyWithinPath); + if (node.FullPathStatus != ResourceNode.PathStatus.Valid) node.FullPath = FullPath.Empty; } return; - static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath) + static ResourceNode.PathStatus GetPathStatus(FullPath fullPath, string? onlyWithinPath) { if (!fullPath.IsRooted) - return true; + return ResourceNode.PathStatus.Valid; if (onlyWithinPath != null) { var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName); if (relPath == ".." || relPath.StartsWith(ParentDirectoryPrefix) || Path.IsPathRooted(relPath)) - return false; + return ResourceNode.PathStatus.External; } - return fullPath.Exists; + return fullPath.Exists + ? ResourceNode.PathStatus.Valid + : ResourceNode.PathStatus.NonExistent; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 7bad64f9..3482f620 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -285,10 +285,10 @@ public class ResourceTreeViewer( } else { - ImGui.Selectable("(unavailable)", false, ImGuiSelectableFlags.Disabled, + ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); ImGuiUtil.HoverTooltip( - $"The actual path to this file is unavailable.\nIt may be managed by another plug-in.{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); + $"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); @@ -354,6 +354,22 @@ public class ResourceTreeViewer( } } + private static ReadOnlySpan GetPathStatusLabel(ResourceNode.PathStatus status) + => status switch + { + ResourceNode.PathStatus.External => "(managed by external tools)"u8, + ResourceNode.PathStatus.NonExistent => "(not found)"u8, + _ => "(unavailable)"u8, + }; + + 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.", + }; + [Flags] private enum TreeCategory : uint { From 4cc5041f0a04dc1066bc00917ce003dd3569b49e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 3 Feb 2025 17:43:44 +0100 Subject: [PATCH 572/865] Improve cleanup. --- Penumbra/Services/CleanupService.cs | 175 +++++++++++++++++++++------- Penumbra/UI/Tabs/SettingsTab.cs | 28 +++-- 2 files changed, 154 insertions(+), 49 deletions(-) diff --git a/Penumbra/Services/CleanupService.cs b/Penumbra/Services/CleanupService.cs index 490c2407..bf76f5f0 100644 --- a/Penumbra/Services/CleanupService.cs +++ b/Penumbra/Services/CleanupService.cs @@ -6,69 +6,160 @@ namespace Penumbra.Services; public class CleanupService(SaveService saveService, ModManager mods, CollectionManager collections) : IService { + private CancellationTokenSource _cancel = new(); + private Task? _task; + + public double Progress { get; private set; } + + public bool IsRunning + => _task is { IsCompleted: false }; + + public void Cancel() + => _cancel.Cancel(); + public void CleanUnusedLocalData() { - var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet(); - foreach (var file in saveService.FileNames.LocalDataFiles.ToList()) - { - try - { - if (!file.Exists || usedFiles.Contains(file.FullName)) - continue; + if (IsRunning) + return; - file.Delete(); - Penumbra.Log.Information($"[CleanupService] Deleted unused local data file {file.Name}."); - } - catch (Exception ex) + var usedFiles = mods.Select(saveService.FileNames.LocalDataFile).ToHashSet(); + Progress = 0; + var deleted = 0; + _cancel = new CancellationTokenSource(); + _task = Task.Run(() => + { + var localFiles = saveService.FileNames.LocalDataFiles.ToList(); + var step = 0.9 / localFiles.Count; + Progress = 0.1; + foreach (var file in localFiles) { - Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}"); + if (_cancel.IsCancellationRequested) + break; + + try + { + if (!file.Exists || usedFiles.Contains(file.FullName)) + continue; + + file.Delete(); + Penumbra.Log.Debug($"[CleanupService] Deleted unused local data file {file.Name}."); + ++deleted; + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete unused local data file {file.Name}:\n{ex}"); + } + + Progress += step; } - } + + Penumbra.Log.Information($"[CleanupService] Deleted {deleted} unused local data files."); + Progress = 1; + }); } public void CleanBackupFiles() { - foreach (var file in mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories)) + if (IsRunning) + return; + + Progress = 0; + var deleted = 0; + _cancel = new CancellationTokenSource(); + _task = Task.Run(() => { - try - { - if (!file.Exists) - continue; + var configFiles = Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories) + .ToList(); + Progress = 0.1; + if (_cancel.IsCancellationRequested) + return; - file.Delete(); - Penumbra.Log.Information($"[CleanupService] Deleted group backup file {file.FullName}."); - } - catch (Exception ex) + var groupFiles = mods.BasePath.EnumerateFiles("group_*.json.bak", SearchOption.AllDirectories).ToList(); + Progress = 0.5; + var step = 0.4 / (groupFiles.Count + configFiles.Count); + foreach (var file in groupFiles) { - Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}"); - } - } + if (_cancel.IsCancellationRequested) + break; - foreach (var file in Directory.EnumerateFiles(saveService.FileNames.ConfigDirectory, "*.json.bak", SearchOption.AllDirectories)) - { - try - { - if (!File.Exists(file)) - continue; + try + { + if (!file.Exists) + continue; - File.Delete(file); - Penumbra.Log.Information($"[CleanupService] Deleted config backup file {file}."); + file.Delete(); + ++deleted; + Penumbra.Log.Debug($"[CleanupService] Deleted group backup file {file.FullName}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete group backup file {file.FullName}:\n{ex}"); + } + + Progress += step; } - catch (Exception ex) + + Penumbra.Log.Information($"[CleanupService] Deleted {deleted} group backup files."); + + deleted = 0; + foreach (var file in configFiles) { - Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}"); + if (_cancel.IsCancellationRequested) + break; + + try + { + if (!File.Exists(file)) + continue; + + File.Delete(file); + ++deleted; + Penumbra.Log.Debug($"[CleanupService] Deleted config backup file {file}."); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[CleanupService] Failed to delete config backup file {file}:\n{ex}"); + } + + Progress += step; } - } + + Penumbra.Log.Information($"[CleanupService] Deleted {deleted} config backup files."); + Progress = 1; + }); } public void CleanupAllUnusedSettings() { - foreach (var collection in collections.Storage) + if (IsRunning) + return; + + Progress = 0; + var totalRemoved = 0; + var diffCollections = 0; + _cancel = new CancellationTokenSource(); + _task = Task.Run(() => { - var count = collections.Storage.CleanUnavailableSettings(collection); - if (count > 0) - Penumbra.Log.Information( - $"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}."); - } + var step = 1.0 / collections.Storage.Count; + foreach (var collection in collections.Storage) + { + if (_cancel.IsCancellationRequested) + break; + + var count = collections.Storage.CleanUnavailableSettings(collection); + if (count > 0) + { + Penumbra.Log.Debug( + $"[CleanupService] Removed {count} unused settings from collection {collection.Identity.AnonymizedName}."); + totalRemoved += count; + ++diffCollections; + } + + Progress += step; + } + + Penumbra.Log.Information($"[CleanupService] Removed {totalRemoved} unused settings from {diffCollections} separate collections."); + Progress = 1; + }); } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index e847b291..9637adeb 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -797,7 +797,6 @@ public class SettingsTab : ITab, IUiService ImGui.Separator(); DrawCleanupButtons(); ImGui.NewLine(); - } private void DrawCrashHandler() @@ -991,24 +990,39 @@ public class SettingsTab : ITab, IUiService private void DrawCleanupButtons() { var enabled = _config.DeleteModModifier.IsActive(); + if (_cleanupService.Progress is not 0.0 and not 1.0) + { + ImUtf8.ProgressBar((float)_cleanupService.Progress, new Vector2(200 * ImUtf8.GlobalScale, ImGui.GetFrameHeight()), + $"{_cleanupService.Progress * 100}%"); + ImGui.SameLine(); + if (ImUtf8.Button("Cancel##FileCleanup"u8)) + _cleanupService.Cancel(); + } + else + { + ImGui.NewLine(); + } + if (ImUtf8.ButtonEx("Clear Unused Local Mod Data Files"u8, - "Delete all local mod data files that do not correspond to currently installed mods."u8, default, !enabled)) + "Delete all local mod data files that do not correspond to currently installed mods."u8, default, + !enabled || _cleanupService.IsRunning)) _cleanupService.CleanUnusedLocalData(); if (!enabled) - ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files."); if (ImUtf8.ButtonEx("Clear Backup Files"u8, "Delete all backups of .json configuration files in your configuration folder and all backups of mod group files in your mod directory."u8, - default, !enabled)) + default, !enabled || _cleanupService.IsRunning)) _cleanupService.CleanBackupFiles(); if (!enabled) - ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to delete files."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to delete files."); if (ImUtf8.ButtonEx("Clear All Unused Settings"u8, - "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default, !enabled)) + "Remove all mod settings in all of your collections that do not correspond to currently installed mods."u8, default, + !enabled || _cleanupService.IsRunning)) _cleanupService.CleanupAllUnusedSettings(); if (!enabled) - ImUtf8.HoverTooltip($"Hold {_config.DeleteModModifier} while clicking to remove settings."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to remove settings."); } /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. From f9952ada75e30a62835d4aa71c1f00e8002f11db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Feb 2025 16:19:57 +0100 Subject: [PATCH 573/865] 1.3.4.0 --- Penumbra/UI/Changelog.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index f83c8989..993ace62 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -57,10 +57,32 @@ public class PenumbraChangelog : IUiService Add1_3_1_0(Changelog); Add1_3_2_0(Changelog); Add1_3_3_0(Changelog); + Add1_3_4_0(Changelog); } #region Changelogs + private static void Add1_3_4_0(Changelog log) + => log.NextVersion("Version 1.3.4.0") + .RegisterHighlight("Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.") + .RegisterEntry("This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", 1) + .RegisterHighlight("Added a new option group type: Combining Groups.") + .RegisterEntry("A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", 1) + .RegisterEntry("Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", 1) + .RegisterEntry("Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.") + .RegisterEntry("Added a display of the number of selected files and folders to the multi mod selection.") + .RegisterEntry("Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.") + .RegisterEntry("Updated the Bone and Material limits in the Model Importer.") + .RegisterEntry("Improved handling of IMC and Material files loaded asynchronously.") + .RegisterEntry("Added IPC functionality to query temporary settings.") + .RegisterEntry("Improved some mod setting IPC functions.") + .RegisterEntry("Fixed some path detection issues in the OnScreen tab.") + .RegisterEntry("Fixed some issues with temporary mod settings.") + .RegisterEntry("Fixed issues with IPC calls before the game has finished loading.") + .RegisterEntry("Fixed using the wrong dye channel in the material editor previews.") + .RegisterEntry("Added some log warnings if outdated materials are loaded by the game.") + .RegisterEntry("Added Schemas for some of the json files generated and read by Penumbra to the solution."); + private static void Add1_3_3_0(Changelog log) => log.NextVersion("Version 1.3.3.0") .RegisterHighlight("Added Temporary Settings to collections.") From 214be98662267915a5fa553b6ed2b159cdc0c597 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 6 Feb 2025 15:55:30 +0000 Subject: [PATCH 574/865] [CI] Updating repo.json for 1.3.4.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 85ba406a..9ee5b00d 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.3.3.1", - "TestingAssemblyVersion": "1.3.3.10", + "AssemblyVersion": "1.3.4.0", + "TestingAssemblyVersion": "1.3.4.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.3.10/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.3.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9b18ffce66d26211e9af57d8afe802d6456b0aea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 6 Feb 2025 16:58:41 +0100 Subject: [PATCH 575/865] Updated submodule Versions. --- Penumbra.Api | 2 +- Penumbra.String | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 35b25bef..c6780905 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 35b25bef92e9b0be96c44c150a3df89d848d2658 +Subproject commit c67809057fac73a0fd407e3ad567f0aa6bc0bc37 diff --git a/Penumbra.String b/Penumbra.String index 0bc2b0f6..4eb7c118 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0bc2b0f66eee1a02c9575b2bb30f27ce166f8632 +Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7 From 50c42078444659cc79157df572b40189d4847913 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Feb 2025 15:12:34 +0100 Subject: [PATCH 576/865] Give messages for unsupported file redirection types. --- Penumbra/Collections/Cache/CollectionCache.cs | 30 ++++++++- .../Cache/CollectionCacheManager.cs | 3 +- .../Interop/PathResolving/PathResolver.cs | 66 +++++++++---------- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 8ca9aa36..3f0ed27b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; @@ -274,6 +275,24 @@ public sealed class CollectionCache : IDisposable _manager.ResolvedFileChanged.Invoke(collection, type, key, value, old, mod); } + private static bool IsRedirectionSupported(Utf8GamePath path, IMod mod) + { + var ext = path.Extension().AsciiToLower().ToString(); + switch (ext) + { + case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc": + Penumbra.Messager.NotificationMessage( + $"Redirection of {ext} files for {mod.Name} is unsupported. Please use the corresponding meta manipulations instead.", + NotificationType.Warning); + return false; + case ".lvb" or ".lgb" or ".sgb": + Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.", + NotificationType.Warning); + return false; + default: return true; + } + } + // Add a specific file redirection, handling potential conflicts. // For different mods, higher mod priority takes precedence before option group priority, // which takes precedence before option priority, which takes precedence before ordering. @@ -283,6 +302,9 @@ public sealed class CollectionCache : IDisposable if (!CheckFullPath(path, file)) return; + if (!IsRedirectionSupported(path, mod)) + return; + try { if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) @@ -342,8 +364,9 @@ public sealed class CollectionCache : IDisposable // Returns if the added mod takes priority before the existing mod. private bool AddConflict(object data, IMod addedMod, IMod existingMod) { - var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; + var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; + var existingPriority = + existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; if (existingPriority < addedPriority) { @@ -427,7 +450,8 @@ public sealed class CollectionCache : IDisposable if (!_changedItems.TryGetValue(name, out var data)) _changedItems.Add(name, (new SingleArray(mod), obj)); else if (!data.Item1.Contains(mod)) - _changedItems[name] = (data.Item1.Append(mod), obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); + _changedItems[name] = (data.Item1.Append(mod), + obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y ? x + y : obj); else if (obj is IdentifiedCounter x && data.Item2 is IdentifiedCounter y) _changedItems[name] = (data.Item1, x + y); } diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c46759c7..ec48e608 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -171,8 +171,7 @@ public class CollectionCacheManager : IDisposable, IService try { ResolvedFileChanged.Invoke(collection, ResolvedFileChanged.Type.FullRecomputeStart, Utf8GamePath.Empty, FullPath.Empty, - FullPath.Empty, - null); + FullPath.Empty, null); cache.ResolvedFiles.Clear(); cache.Meta.Reset(); cache.ConflictDict.Clear(); diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 0b6c8340..8e5504d5 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -49,42 +49,38 @@ public class PathResolver : IDisposable, IService if (!_config.EnableMods) return (null, ResolveData.Invalid); - // Do not allow manipulating layers to prevent very obvious cheating and softlocks. - if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb) - return (null, ResolveData.Invalid); - - // Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently. - if (resourceType is ResourceType.Atch) - return ResolveAtch(path); - - return category switch + return resourceType switch { - // Only Interface collection. - ResourceCategory.Ui => ResolveUi(path), - // Never allow changing scripts. - ResourceCategory.UiScript => (null, ResolveData.Invalid), - ResourceCategory.GameScript => (null, ResolveData.Invalid), - // Use actual resolving. - ResourceCategory.Chara => Resolve(path, resourceType), - ResourceCategory.Shader => ResolveShader(path, resourceType), - ResourceCategory.Vfx => Resolve(path, resourceType), - ResourceCategory.Sound => Resolve(path, resourceType), - // EXD Modding in general should probably be prohibited but is currently used for fan translations. - // We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code. - ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8) - ? (null, ResolveData.Invalid) - : DefaultResolver(path), - // None of these files are ever associated with specific characters, - // always use the default resolver for now, - // except that common/font is conceptually more UI. - ResourceCategory.Common => path.Path.StartsWith("common/font"u8) - ? ResolveUi(path) - : DefaultResolver(path), - ResourceCategory.BgCommon => DefaultResolver(path), - ResourceCategory.Bg => DefaultResolver(path), - ResourceCategory.Cut => DefaultResolver(path), - ResourceCategory.Music => DefaultResolver(path), - _ => DefaultResolver(path), + // Do not allow manipulating layers to prevent very obvious cheating and softlocks. + ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb => (null, ResolveData.Invalid), + // Prevent .atch loading to prevent crashes on outdated .atch files. + ResourceType.Atch => ResolveAtch(path), + + _ => category switch + { + // Only Interface collection. + ResourceCategory.Ui => ResolveUi(path), + // Never allow changing scripts. + ResourceCategory.UiScript => (null, ResolveData.Invalid), + ResourceCategory.GameScript => (null, ResolveData.Invalid), + // Use actual resolving. + ResourceCategory.Chara => Resolve(path, resourceType), + ResourceCategory.Shader => ResolveShader(path, resourceType), + ResourceCategory.Vfx => Resolve(path, resourceType), + ResourceCategory.Sound => Resolve(path, resourceType), + // EXD Modding in general should probably be prohibited but is currently used for fan translations. + // We prevent WebURL specifically because it technically allows launching arbitrary programs / to execute arbitrary code. + ResourceCategory.Exd => path.Path.StartsWith("exd/weburl"u8) ? (null, ResolveData.Invalid) : DefaultResolver(path), + // None of these files are ever associated with specific characters, + // always use the default resolver for now, + // except that common/font is conceptually more UI. + ResourceCategory.Common => path.Path.StartsWith("common/font"u8) ? ResolveUi(path) : DefaultResolver(path), + ResourceCategory.BgCommon => DefaultResolver(path), + ResourceCategory.Bg => DefaultResolver(path), + ResourceCategory.Cut => DefaultResolver(path), + ResourceCategory.Music => DefaultResolver(path), + _ => DefaultResolver(path), + } }; } From 60b9facea31c871d5951791ae8bffbc9ee9337fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Feb 2025 15:27:32 +0100 Subject: [PATCH 577/865] Cont. --- Penumbra/Interop/PathResolving/PathResolver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 8e5504d5..ec421304 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,4 +1,3 @@ -using System.Linq; using FFXIVClientStructs.FFXIV.Client.System.Resource; using OtterGui.Services; using Penumbra.Api.Enums; @@ -55,6 +54,8 @@ public class PathResolver : IDisposable, IService ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb => (null, ResolveData.Invalid), // Prevent .atch loading to prevent crashes on outdated .atch files. ResourceType.Atch => ResolveAtch(path), + // These are manipulated through Meta Edits instead. + ResourceType.Eqp or ResourceType.Eqdp or ResourceType.Est or ResourceType.Gmp or ResourceType.Cmp => (null, ResolveData.Invalid), _ => category switch { From 0af9667789d763a6c3a512a605d41b77925e2b1f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Feb 2025 16:44:22 +0100 Subject: [PATCH 578/865] Add changed item adapters. --- Penumbra.Api | 2 +- Penumbra/Api/Api/ModsApi.cs | 6 ++ Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 + Penumbra/Mods/Manager/ModCacheManager.cs | 2 +- .../Mods/Manager/ModChangedItemAdapter.cs | 102 ++++++++++++++++++ 6 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 Penumbra/Mods/Manager/ModChangedItemAdapter.cs diff --git a/Penumbra.Api b/Penumbra.Api index c6780905..7ae46f0d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c67809057fac73a0fd407e3ad567f0aa6bc0bc37 +Subproject commit 7ae46f0d09f40b36a5b2d10382db46fbfb729117 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index 64e201be..ace98f83 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -145,4 +145,10 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable => _modManager.TryGetMod(modDirectory, modName, out var mod) ? mod.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToInternalObject()) : []; + + public IReadOnlyDictionary> GetChangedItemAdapterDictionary() + => new ModChangedItemAdapter(new WeakReference(_modManager)); + + public IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> GetChangedItemAdapterList() + => new ModChangedItemAdapter(new WeakReference(_modManager)); } diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index cfc9d470..36f799a0 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 6); + => (5, 7); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 9733f82e..085e57ca 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetModPath.Provider(pi, api.Mods), IpcSubscribers.SetModPath.Provider(pi, api.Mods), IpcSubscribers.GetChangedItems.Provider(pi, api.Mods), + IpcSubscribers.GetChangedItemAdapterDictionary.Provider(pi, api.Mods), + IpcSubscribers.GetChangedItemAdapterList.Provider(pi, api.Mods), IpcSubscribers.GetAvailableModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 38d98d7c..4bf22272 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -15,7 +15,7 @@ public class ModCacheManager : IDisposable, IService private readonly CommunicatorService _communicator; private readonly ObjectIdentification _identifier; private readonly ModStorage _modManager; - private bool _updatingItems = false; + private bool _updatingItems; public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config) { diff --git a/Penumbra/Mods/Manager/ModChangedItemAdapter.cs b/Penumbra/Mods/Manager/ModChangedItemAdapter.cs new file mode 100644 index 00000000..8b99cdf2 --- /dev/null +++ b/Penumbra/Mods/Manager/ModChangedItemAdapter.cs @@ -0,0 +1,102 @@ +using Penumbra.GameData.Data; + +namespace Penumbra.Mods.Manager; + +public sealed class ModChangedItemAdapter(WeakReference storage) + : IReadOnlyDictionary>, + IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)> +{ + IEnumerator<(string ModDirectory, IReadOnlyDictionary ChangedItems)> + IEnumerable<(string ModDirectory, IReadOnlyDictionary ChangedItems)>.GetEnumerator() + => Storage.Select(m => (m.Identifier, (IReadOnlyDictionary)new ChangedItemDictionaryAdapter(m.ChangedItems))) + .GetEnumerator(); + + public IEnumerator>> GetEnumerator() + => Storage.Select(m => new KeyValuePair>(m.Identifier, + new ChangedItemDictionaryAdapter(m.ChangedItems))) + .GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => Storage.Count; + + public bool ContainsKey(string key) + => Storage.TryGetMod(key, string.Empty, out _); + + public bool TryGetValue(string key, [NotNullWhen(true)] out IReadOnlyDictionary? value) + { + if (Storage.TryGetMod(key, string.Empty, out var mod)) + { + value = new ChangedItemDictionaryAdapter(mod.ChangedItems); + return true; + } + + value = null; + return false; + } + + public IReadOnlyDictionary this[string key] + => TryGetValue(key, out var v) ? v : throw new KeyNotFoundException(); + + (string ModDirectory, IReadOnlyDictionary ChangedItems) + IReadOnlyList<(string ModDirectory, IReadOnlyDictionary ChangedItems)>.this[int index] + { + get + { + var m = Storage[index]; + return (m.Identifier, new ChangedItemDictionaryAdapter(m.ChangedItems)); + } + } + + public IEnumerable Keys + => Storage.Select(m => m.Identifier); + + public IEnumerable> Values + => Storage.Select(m => new ChangedItemDictionaryAdapter(m.ChangedItems)); + + private ModStorage Storage + { + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + get => storage.TryGetTarget(out var t) + ? t + : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed."); + } + + private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary + { + public IEnumerator> GetEnumerator() + => data.Select(d => new KeyValuePair(d.Key, d.Value?.ToInternalObject())).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => data.Count; + + public bool ContainsKey(string key) + => data.ContainsKey(key); + + public bool TryGetValue(string key, out object? value) + { + if (data.TryGetValue(key, out var v)) + { + value = v?.ToInternalObject(); + return true; + } + + value = null; + return false; + } + + public object? this[string key] + => data[key]?.ToInternalObject(); + + public IEnumerable Keys + => data.Keys; + + public IEnumerable Values + => data.Values.Select(v => v?.ToInternalObject()); + } +} From a9a556eb55a2ed6c93899eb47926663301da2066 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 12 Feb 2025 17:56:01 +0100 Subject: [PATCH 579/865] Add CheckCurrentChangedItemFunc, --- Penumbra.Api | 2 +- Penumbra/Api/Api/CollectionApi.cs | 22 +++++++++++++++++-- Penumbra/Api/IpcProviders.cs | 1 + .../Manager => Api}/ModChangedItemAdapter.cs | 3 ++- 4 files changed, 24 insertions(+), 4 deletions(-) rename Penumbra/{Mods/Manager => Api}/ModChangedItemAdapter.cs (95%) diff --git a/Penumbra.Api b/Penumbra.Api index 7ae46f0d..70f04683 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 7ae46f0d09f40b36a5b2d10382db46fbfb729117 +Subproject commit 70f046830cc7cd35b3480b12b7efe94182477fbb diff --git a/Penumbra/Api/Api/CollectionApi.cs b/Penumbra/Api/Api/CollectionApi.cs index 964da1a5..c40feb12 100644 --- a/Penumbra/Api/Api/CollectionApi.cs +++ b/Penumbra/Api/Api/CollectionApi.cs @@ -2,6 +2,7 @@ using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; +using Penumbra.Mods; namespace Penumbra.Api.Api; @@ -23,11 +24,27 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : .Select(c => (c.Identity.Id, c.Identity.Name))); list.AddRange(collections.Storage - .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase) && !list.Contains((c.Identity.Id, c.Identity.Name))) + .Where(c => string.Equals(c.Identity.Name, identifier, StringComparison.OrdinalIgnoreCase) + && !list.Contains((c.Identity.Id, c.Identity.Name))) .Select(c => (c.Identity.Id, c.Identity.Name))); return list; } + public Func CheckCurrentChangedItemFunc() + { + var weakRef = new WeakReference(collections); + return s => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying collection storage of this IPC container was disposed."); + + if (!c.Active.Current.ChangedItems.TryGetValue(s, out var d)) + return []; + + return d.Item1.Select(m => (m is Mod mod ? mod.Identifier : string.Empty, m.Name.Text)).ToArray(); + }; + } + public Dictionary GetChangedItemsForCollection(Guid collectionId) { try @@ -74,7 +91,8 @@ public class CollectionApi(CollectionManager collections, ApiHelpers helpers) : } public Guid[] GetCollectionByName(string name) - => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id).ToArray(); + => collections.Storage.Where(c => string.Equals(name, c.Identity.Name, StringComparison.OrdinalIgnoreCase)).Select(c => c.Identity.Id) + .ToArray(); public (PenumbraApiEc, (Guid Id, string Name)? OldCollection) SetCollection(ApiCollectionType type, Guid? collectionId, bool allowCreateNew, bool allowDelete) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 085e57ca..d54faa6c 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -29,6 +29,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetCollectionForObject.Provider(pi, api.Collection), IpcSubscribers.SetCollection.Provider(pi, api.Collection), IpcSubscribers.SetCollectionForObject.Provider(pi, api.Collection), + IpcSubscribers.CheckCurrentChangedItemFunc.Provider(pi, api.Collection), IpcSubscribers.ConvertTextureFile.Provider(pi, api.Editing), IpcSubscribers.ConvertTextureData.Provider(pi, api.Editing), diff --git a/Penumbra/Mods/Manager/ModChangedItemAdapter.cs b/Penumbra/Api/ModChangedItemAdapter.cs similarity index 95% rename from Penumbra/Mods/Manager/ModChangedItemAdapter.cs rename to Penumbra/Api/ModChangedItemAdapter.cs index 8b99cdf2..8842f20a 100644 --- a/Penumbra/Mods/Manager/ModChangedItemAdapter.cs +++ b/Penumbra/Api/ModChangedItemAdapter.cs @@ -1,6 +1,7 @@ using Penumbra.GameData.Data; +using Penumbra.Mods.Manager; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Api; public sealed class ModChangedItemAdapter(WeakReference storage) : IReadOnlyDictionary>, From f89eab8b2bd2b08c48b6608b2b62e042d610f3cc Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 13 Feb 2025 15:42:58 +0000 Subject: [PATCH 580/865] [CI] Updating repo.json for testing_1.3.4.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 9ee5b00d..c5402b49 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.0", + "TestingAssemblyVersion": "1.3.4.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 2be5bd06117caa208c51c27a68550a773075abf6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Feb 2025 16:45:46 +0100 Subject: [PATCH 581/865] Make EQP swaps also swap multi-slot items correctly. --- Penumbra.GameData | 2 +- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 134 +++++++++++++------- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 4 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 10 +- 4 files changed, 93 insertions(+), 57 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 4a987167..f6dff467 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4a987167b665184d4c05fc9863993981c35a1d19 +Subproject commit f6dff467c7dad6b1213a7d7b65d40a56450f0672 diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index c7e43a26..8c80c91c 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -27,31 +27,31 @@ public static class EquipmentSwap : []; } - public static EquipItem[] CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, + public static HashSet CreateTypeSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, Func redirections, MetaDictionary manips, EquipSlot slotFrom, EquipItem itemFrom, EquipSlot slotTo, EquipItem itemTo) { LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo); if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot()) throw new ItemSwap.InvalidItemTypeException(); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); var mtrlVariantTo = imcEntry.MaterialId; - var skipFemale = false; - var skipMale = false; + var skipFemale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -88,30 +88,40 @@ public static class EquipmentSwap return affectedItems; } - public static EquipItem[] CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, + public static HashSet CreateItemSwap(MetaFileManager manager, ObjectIdentification identifier, List swaps, Func redirections, MetaDictionary manips, EquipItem itemFrom, EquipItem itemTo, bool rFinger = true, bool lFinger = true) { // Check actual ids, variants and slots. We only support using the same slot. LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom); - LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); + LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo); if (slotFrom != slotTo) throw new ItemSwap.InvalidItemTypeException(); - var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo); + HashSet affectedItems = []; + var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo); if (eqp != null) + { swaps.Add(eqp); + // Add items affected through multi-slot EQP edits. + foreach (var child in eqp.ChildSwaps.SelectMany(c => c.WithChildren()).OfType>()) + { + affectedItems.UnionWith(identifier + .Identify(GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, child.SwapFromIdentifier.Slot)) + .Select(kvp => kvp.Value).OfType().Select(i => i.Item)); + } + } var gmp = CreateGmp(manager, manips, slotFrom, idFrom, idTo); if (gmp != null) swaps.Add(gmp); - var affectedItems = Array.Empty(); foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger)) { - (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); + var (imcFileFrom, variants, affectedItemsLocal) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom); + affectedItems.UnionWith(affectedItemsLocal); var imcIdentifierTo = new ImcIdentifier(slotTo, idTo, variantTo); - var imcFileTo = new ImcFile(manager, imcIdentifierTo); + var imcFileTo = new ImcFile(manager, imcIdentifierTo); var imcEntry = manips.TryGetValue(imcIdentifierTo, out var entry) ? entry : imcFileTo.GetEntry(imcIdentifierTo.EquipSlot, imcIdentifierTo.Variant); @@ -122,18 +132,18 @@ public static class EquipmentSwap { EquipSlot.Head => EstType.Head, EquipSlot.Body => EstType.Body, - _ => (EstType)0, + _ => (EstType)0, }; var skipFemale = false; - var skipMale = false; + var skipMale = false; foreach (var gr in Enum.GetValues()) { switch (gr.Split().Item1) { - case Gender.Male when skipMale: continue; - case Gender.Female when skipFemale: continue; - case Gender.MaleNpc when skipMale: continue; + case Gender.Male when skipMale: continue; + case Gender.Female when skipFemale: continue; + case Gender.MaleNpc when skipMale: continue; case Gender.FemaleNpc when skipFemale: continue; } @@ -148,7 +158,7 @@ public static class EquipmentSwap swaps.Add(eqdp); var ownMdl = eqdp?.SwapToModdedEntry.Model ?? false; - var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); + var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl); if (est != null) swaps.Add(est); } @@ -176,6 +186,7 @@ public static class EquipmentSwap return affectedItems; } + public static MetaSwap? CreateEqdp(MetaFileManager manager, Func redirections, MetaDictionary manips, EquipSlot slot, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo); @@ -185,9 +196,9 @@ public static class EquipmentSwap PrimaryId idTo, byte mtrlTo) { var eqdpFromIdentifier = new EqdpIdentifier(idFrom, slotFrom, gr); - var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); - var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); - var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); + var eqdpToIdentifier = new EqdpIdentifier(idTo, slotTo, gr); + var eqdpFromDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpFromIdentifier), slotFrom); + var eqdpToDefault = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(manager, eqdpToIdentifier), slotTo); var meta = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, eqdpFromIdentifier, eqdpFromDefault, eqdpToIdentifier, eqdpToDefault); @@ -216,7 +227,7 @@ public static class EquipmentSwap ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -238,16 +249,17 @@ public static class EquipmentSwap variant = i.Variant; } - private static (ImcFile, Variant[], EquipItem[]) GetVariants(MetaFileManager manager, ObjectIdentification identifier, EquipSlot slotFrom, + private static (ImcFile, Variant[], HashSet) GetVariants(MetaFileManager manager, ObjectIdentification identifier, + EquipSlot slotFrom, PrimaryId idFrom, PrimaryId idTo, Variant variantFrom) { - var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var imc = new ImcFile(manager, ident); - EquipItem[] items; - Variant[] variants; + var ident = new ImcIdentifier(slotFrom, idFrom, variantFrom); + var imc = new ImcFile(manager, ident); + HashSet items; + Variant[] variants; if (idFrom == idTo) { - items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToArray(); + items = identifier.Identify(idFrom, 0, variantFrom, slotFrom).ToHashSet(); variants = [variantFrom]; } else @@ -256,7 +268,7 @@ public static class EquipmentSwap ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) .Select(kvp => kvp.Value).OfType().Select(i => i.Item) - .ToArray(); + .ToHashSet(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); } @@ -270,9 +282,9 @@ public static class EquipmentSwap return null; var manipFromIdentifier = new GmpIdentifier(idFrom); - var manipToIdentifier = new GmpIdentifier(idTo); - var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); - var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); + var manipToIdentifier = new GmpIdentifier(idTo); + var manipFromDefault = ExpandedGmpFile.GetDefault(manager, manipFromIdentifier); + var manipToDefault = ExpandedGmpFile.GetDefault(manager, manipToIdentifier); return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); } @@ -287,9 +299,9 @@ public static class EquipmentSwap Variant variantFrom, Variant variantTo, ImcFile imcFileFrom, ImcFile imcFileTo) { var manipFromIdentifier = new ImcIdentifier(slotFrom, idFrom, variantFrom); - var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); - var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); - var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); + var manipToIdentifier = new ImcIdentifier(slotTo, idTo, variantTo); + var manipFromDefault = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom); + var manipToDefault = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo); var imc = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); @@ -328,7 +340,7 @@ public static class EquipmentSwap var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); - var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); + var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) { @@ -339,18 +351,42 @@ public static class EquipmentSwap return avfx; } - public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, - EquipSlot slot, PrimaryId idFrom, PrimaryId idTo) + public static MetaSwap? CreateEqp(MetaFileManager manager, MetaDictionary manips, EquipSlot slot, + PrimaryId idFrom, PrimaryId idTo) { if (slot.IsAccessory()) return null; var manipFromIdentifier = new EqpIdentifier(idFrom, slot); - var manipToIdentifier = new EqpIdentifier(idTo, slot); - var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); - var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); - return new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, + var manipToIdentifier = new EqpIdentifier(idTo, slot); + var manipFromDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idFrom), slot); + var manipToDefault = new EqpEntryInternal(ExpandedEqpFile.GetDefault(manager, idTo), slot); + var swap = new MetaSwap(i => manips.TryGetValue(i, out var e) ? e : null, manipFromIdentifier, manipFromDefault, manipToIdentifier, manipToDefault); + var entry = swap.SwapToModdedEntry.ToEntry(slot); + // Add additional EQP entries if the swapped item is a multi-slot item, + // because those take the EQP entries of their other model-set slots when used. + switch (slot) + { + case EquipSlot.Body: + if (!entry.HasFlag(EqpEntry.BodyShowLeg) + && CreateEqp(manager, manips, EquipSlot.Legs, idFrom, idTo) is { } legChild) + swap.ChildSwaps.Add(legChild); + if (!entry.HasFlag(EqpEntry.BodyShowHead) + && CreateEqp(manager, manips, EquipSlot.Head, idFrom, idTo) is { } headChild) + swap.ChildSwaps.Add(headChild); + if (!entry.HasFlag(EqpEntry.BodyShowHand) + && CreateEqp(manager, manips, EquipSlot.Hands, idFrom, idTo) is { } handChild) + swap.ChildSwaps.Add(handChild); + break; + case EquipSlot.Legs: + if (!entry.HasFlag(EqpEntry.LegsShowFoot) + && CreateEqp(manager, manips, EquipSlot.Feet, idFrom, idTo) is { } footChild) + swap.ChildSwaps.Add(footChild); + break; + } + + return swap; } public static FileSwap? CreateMtrl(MetaFileManager manager, Func redirections, EquipSlot slot, PrimaryId idFrom, @@ -380,7 +416,7 @@ public static class EquipmentSwap if (newFileName != fileName) { - fileName = newFileName; + fileName = newFileName; dataWasChanged = true; } @@ -405,13 +441,13 @@ public static class EquipmentSwap EquipSlot slotTo, PrimaryId idFrom, PrimaryId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged) { var addedDashes = GamePaths.Tex.HandleDx11Path(texture, out var path); - var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); + var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom); newPath = ItemSwap.ReplaceType(newPath, slotFrom, slotTo, idFrom); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}"); if (newPath != path) { - texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; + texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath; dataWasChanged = true; } @@ -429,8 +465,8 @@ public static class EquipmentSwap PrimaryId idFrom, ref string filePath, ref bool dataWasChanged) { var oldPath = filePath; - filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); - filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); + filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}"); + filePath = ItemSwap.ReplaceType(filePath, slotFrom, slotTo, idFrom); dataWasChanged = true; return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath); diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index d2deb9ef..a9d5e0d6 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -127,7 +127,7 @@ public class ItemSwapContainer ? new MetaDictionary(cache) : _appliedModData.Manipulations; - public EquipItem[] LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, + public HashSet LoadEquipment(EquipItem from, EquipItem to, ModCollection? collection = null, bool useRightRing = true, bool useLeftRing = true) { Swaps.Clear(); @@ -138,7 +138,7 @@ public class ItemSwapContainer return ret; } - public EquipItem[] LoadTypeSwap(EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null) + public HashSet LoadTypeSwap(EquipSlot slotFrom, EquipItem from, EquipSlot slotTo, EquipItem to, ModCollection? collection = null) { Swaps.Clear(); Loaded = false; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index e590eb1e..cb56de08 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -180,7 +180,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService private bool _useLeftRing = true; private bool _useRightRing = true; - private EquipItem[]? _affectedItems; + private HashSet? _affectedItems; private void UpdateState() { @@ -541,11 +541,11 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * UiHelpers.Scale, ImGui.GetTextLineHeightWithSpacing()); - if (_affectedItems is not { Length: > 1 }) + if (_affectedItems is not { Count: > 1 }) return; ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)) @@ -602,11 +602,11 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing); } - if (_affectedItems is not { Length: > 1 }) + if (_affectedItems is not { Count: > 1 }) return; ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, + ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg); if (ImGui.IsItemHovered()) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)) From 93e184c9a564302dde5434369028033dae037eb3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Feb 2025 16:46:03 +0100 Subject: [PATCH 582/865] Add import of .atch files into metadata. --- .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 45 +++++++++++++++++-- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 25 ++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 4cf01faa..66db0932 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -1,11 +1,14 @@ using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Newtonsoft.Json.Linq; +using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; @@ -13,6 +16,7 @@ using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; namespace Penumbra.UI.AdvancedWindow.Meta; @@ -27,9 +31,9 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer public override float ColumnHeight => 2 * ImUtf8.FrameHeightSpacing; - private AtchFile? _currentBaseAtchFile; - private AtchPoint? _currentBaseAtchPoint; - private AtchPointCombo _combo; + private AtchFile? _currentBaseAtchFile; + private AtchPoint? _currentBaseAtchPoint; + private readonly AtchPointCombo _combo; public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : base(editor, metaFiles) @@ -44,6 +48,41 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer => obj.ToName(); } + public void ImportFile(string filePath) + { + try + { + if (filePath.Length == 0 || !File.Exists(filePath)) + throw new FileNotFoundException(); + + var gr = GamePaths.ParseRaceCode(filePath); + if (gr is GenderRace.Unknown) + throw new Exception($"Could not identify race code from path {filePath}."); + var text = File.ReadAllBytes(filePath); + var file = new AtchFile(text); + foreach (var point in file.Points) + { + foreach (var (entry, index) in point.Entries.WithIndex()) + { + var identifier = new AtchIdentifier(point.Type, gr, (ushort) index); + var defaultValue = AtchCache.GetDefault(MetaFiles, identifier); + if (defaultValue == null) + continue; + + if (defaultValue.Value.Equals(entry)) + Editor.Changes |= Editor.Remove(identifier); + else + Editor.Changes |= Editor.TryAdd(identifier, entry) || Editor.Update(identifier, entry); + } + } + } + catch (Exception ex) + { + Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:", + NotificationType.Warning)); + } + } + protected override void DrawNew() { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 7d688df9..1356340c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -4,6 +4,8 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Api; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.UI.AdvancedWindow.Meta; using Penumbra.UI.Classes; @@ -43,8 +45,10 @@ public partial class ModEditWindow if (ImUtf8.Button("Write as TexTools Files"u8)) _metaFileManager.WriteAllTexToolsMeta(Mod!); ImGui.SameLine(); - if (ImUtf8.ButtonEx("Remove All Default-Values", "Delete any entries from all lists that set the value to its default value."u8)) + if (ImUtf8.ButtonEx("Remove All Default-Values"u8, "Delete any entries from all lists that set the value to its default value."u8)) _editor.MetaEditor.DeleteDefaultValues(); + ImGui.SameLine(); + DrawAtchDragDrop(); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) @@ -60,6 +64,25 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.GlobalEqp); } + private void DrawAtchDragDrop() + { + _dragDropManager.CreateImGuiSource("atchDrag", f => f.Extensions.Contains(".atch"), f => + { + var gr = GamePaths.ParseRaceCode(f.Files.FirstOrDefault() ?? string.Empty); + if (gr is GenderRace.Unknown) + return false; + + ImUtf8.Text($"Dragging .atch for {gr.ToName()}..."); + return true; + }); + ImUtf8.ButtonEx("Import .atch"u8, + _dragDropManager.IsDragging ? ""u8 : "Drag a .atch file containinig its race code in the path here to import its values."u8, + default, + !_dragDropManager.IsDragging); + if (_dragDropManager.CreateImGuiTarget("atchDrag", out var files, out _) && files.FirstOrDefault() is { } file) + _metaDrawers.Atch.ImportFile(file); + } + private void DrawEditHeader(MetaManipulationType type) { var drawer = _metaDrawers.Get(type); From 40f24344af122eb4bb8bc3e0d5afdcd9c3395477 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 15 Feb 2025 16:46:31 +0100 Subject: [PATCH 583/865] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 3c1260c9..0b6085ce 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3c1260c9833303c2d33d12d6f77dc2b1afea3f34 +Subproject commit 0b6085ce720ffb7c78cf42d4e51861f34db27744 From 79938b6dd01a914cb13dcd833c3f2d97de0372da Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 15 Feb 2025 15:50:18 +0000 Subject: [PATCH 584/865] [CI] Updating repo.json for testing_1.3.4.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c5402b49..fdb0b638 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.1", + "TestingAssemblyVersion": "1.3.4.2", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.2/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From b7b9defaa6f8d861841cb134dfbb3ef161c24bda Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Feb 2025 17:38:42 +0100 Subject: [PATCH 585/865] Add context menu to clear temporary settings. --- Penumbra/Api/Api/TemporaryApi.cs | 14 +++----------- Penumbra/Collections/Manager/CollectionEditor.cs | 14 ++++++++++++++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 9 ++++++++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index d951639c..a997ded8 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -130,8 +130,8 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } - public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, string modDirectory, - string modName, int key) + public (PenumbraApiEc, (bool, bool, int, Dictionary>)?, string) QueryTemporaryModSettings(Guid collectionId, + string modDirectory, string modName, int key) { var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName); if (!collectionManager.Storage.ById(collectionId, out var collection)) @@ -296,15 +296,7 @@ public class TemporaryApi( if (collection.Identity.Index <= 0) return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); - var numRemoved = 0; - for (var i = 0; i < collection.Settings.Count; ++i) - { - if (collection.GetTempSettings(i) is { } tempSettings - && tempSettings.Lock == key - && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) - ++numRemoved; - } - + var numRemoved = collectionManager.Editor.ClearTemporarySettings(collection, key); return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); } diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index f4902fda..5ccc38e2 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -114,6 +114,20 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public int ClearTemporarySettings(ModCollection collection, int key = 0) + { + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is { } tempSettings + && tempSettings.Lock == key + && SetTemporarySettings(collection, modStorage[i], null, key)) + ++numRemoved; + } + + return numRemoved; + } + public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) { var old = collection.GetTempSettings(mod.Index); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 1a7d4e31..0a68c077 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -64,6 +64,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector QuickMove(l, _config.QuickMoveFolder1, _config.QuickMoveFolder2, _config.QuickMoveFolder3)); + SubscribeRightClickMain(ClearTemporarySettings, 105); SubscribeRightClickMain(ClearDefaultImportFolder, 100); SubscribeRightClickMain(() => ClearQuickMove(0, _config.QuickMoveFolder1, () => {_config.QuickMoveFolder1 = string.Empty; _config.Save();}), 110); SubscribeRightClickMain(() => ClearQuickMove(1, _config.QuickMoveFolder2, () => {_config.QuickMoveFolder2 = string.Empty; _config.Save();}), 120); @@ -237,10 +238,16 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Mon, 17 Feb 2025 17:39:41 +0100 Subject: [PATCH 586/865] Add option to always work in temporary settings. --- Penumbra/Configuration.cs | 1 + .../Mods/Settings/TemporaryModSettings.cs | 9 ++-- Penumbra/UI/Classes/CollectionSelectHeader.cs | 43 ++++++++++++++++--- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 9 ++-- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 11 +++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 27 ++++++------ Penumbra/UI/Tabs/SettingsTab.cs | 4 +- 7 files changed, 72 insertions(+), 32 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index df44a51a..ce86dd4a 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -70,6 +70,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public bool DefaultTemporaryMode { get; set; } = false; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index fa71e1b6..a16a9feb 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -2,9 +2,10 @@ namespace Penumbra.Mods.Settings; public sealed class TemporaryModSettings : ModSettings { - public string Source = string.Empty; - public int Lock = 0; - public bool ForceInherit; + public const string OwnSource = "yourself"; + public string Source = string.Empty; + public int Lock = 0; + public bool ForceInherit; // Create default settings for a given mod. public static TemporaryModSettings DefaultSettings(Mod mod, string source, bool enabled = false, int key = 0) @@ -20,7 +21,7 @@ public sealed class TemporaryModSettings : ModSettings public TemporaryModSettings() { } - public TemporaryModSettings(Mod mod, ModSettings? clone, string source, int key = 0) + public TemporaryModSettings(Mod mod, ModSettings? clone, string source = OwnSource, int key = 0) { Source = source; Lock = key; diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 0e1408c5..54fcf279 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,7 +1,10 @@ +using Dalamud.Interface; using ImGuiNET; using OtterGui.Raii; using OtterGui; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.Widget; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -12,18 +15,21 @@ namespace Penumbra.UI.Classes; public class CollectionSelectHeader : IUiService { - private readonly CollectionCombo _collectionCombo; - private readonly ActiveCollections _activeCollections; - private readonly TutorialService _tutorial; - private readonly ModSelection _selection; - private readonly CollectionResolver _resolver; + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModSelection _selection; + private readonly CollectionResolver _resolver; + private readonly FontAwesomeCheckbox _temporaryCheckbox = new(FontAwesomeIcon.Stopwatch); + private readonly Configuration _config; public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection, - CollectionResolver resolver) + CollectionResolver resolver, Configuration config) { _tutorial = tutorial; _selection = selection; _resolver = resolver; + _config = config; _activeCollections = collectionManager.Active; _collectionCombo = new CollectionCombo(collectionManager, () => collectionManager.Storage.OrderBy(c => c.Identity.Name).ToList()); } @@ -33,6 +39,8 @@ public class CollectionSelectHeader : IUiService { using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) .Push(ImGuiStyleVar.ItemSpacing, new Vector2(0, spacing ? ImGui.GetStyle().ItemSpacing.Y : 0)); + DrawTemporaryCheckbox(); + ImGui.SameLine(); var comboWidth = ImGui.GetContentRegionAvail().X / 4f; var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); using (var _ = ImRaii.Group()) @@ -51,6 +59,29 @@ public class CollectionSelectHeader : IUiService ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); } + private void DrawTemporaryCheckbox() + { + var hold = _config.DeleteModModifier.IsActive(); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) + { + var tint = ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint); + using var color = ImRaii.PushColor(ImGuiCol.FrameBgHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.FrameBgActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.CheckMark, tint) + .Push(ImGuiCol.Border, tint, _config.DefaultTemporaryMode); + if (_temporaryCheckbox.Draw("##tempCheck"u8, _config.DefaultTemporaryMode, out var newValue) && hold) + { + _config.DefaultTemporaryMode = newValue; + _config.Save(); + } + } + + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired.\n"u8); + if (!hold) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to toggle."); + } + private enum CollectionState { Empty, diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index b723978b..8dee13bf 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -20,6 +20,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private bool _temporary; private bool _locked; private TemporaryModSettings? _tempSettings; + private ModSettings? _settings; public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { @@ -27,6 +28,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle return; _blockGroupCache.Clear(); + _settings = settings; _tempSettings = tempSettings; _temporary = tempSettings != null; _locked = (tempSettings?.Lock ?? 0) > 0; @@ -242,10 +244,11 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) { - if (_temporary) + if (_temporary || config.DefaultTemporaryMode) { - _tempSettings!.ForceInherit = false; - _tempSettings!.Settings[groupIdx] = setting; + _tempSettings ??= new TemporaryModSettings(group.Mod, _settings); + _tempSettings!.ForceInherit = false; + _tempSettings!.Settings[groupIdx] = setting; collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); } else diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0a68c077..a0383329 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -274,8 +274,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector.Leaf mod) { - const string source = "yourself"; - var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); + var tempSettings = _collectionManager.Active.Current.GetTempSettings(mod.Value.Index); if (tempSettings is { Lock: > 0 }) return; @@ -284,19 +283,19 @@ public sealed class ModFileSystemSelector : FileSystemSelector _config.PrintSuccessfulCommandsToChat = v); @@ -618,6 +617,9 @@ public class SettingsTab : ITab, IUiService /// Draw all settings pertaining to import and export of mods. private void DrawModHandlingSettings() { + Checkbox("Use Temporary Settings Per Default", + "When you make any changes to your collection, apply them as temporary changes first and require a click to 'turn permanent' if you want to keep them.", + _config.DefaultTemporaryMode, v => _config.DefaultTemporaryMode = v); Checkbox("Replace Non-Standard Symbols On Import", "Replace all non-ASCII symbols in mod and option names with underscores when importing mods.", _config.ReplaceNonAsciiOnImport, v => _config.ReplaceNonAsciiOnImport = v); From 41672c31ce0e63f07c3c46e9c903ae05442dc0a1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 18 Feb 2025 15:10:16 +0100 Subject: [PATCH 587/865] Update message slightly. --- Penumbra/Collections/Cache/CollectionCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 3f0ed27b..a80928d0 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -282,11 +282,11 @@ public sealed class CollectionCache : IDisposable { case ".atch" or ".eqp" or ".eqdp" or ".est" or ".gmp" or ".cmp" or ".imc": Penumbra.Messager.NotificationMessage( - $"Redirection of {ext} files for {mod.Name} is unsupported. Please use the corresponding meta manipulations instead.", + $"Redirection of {ext} files for {mod.Name} is unsupported. This probably means that the mod is outdated and may not work correctly.\n\nPlease tell the mod creator to use the corresponding meta manipulations instead.", NotificationType.Warning); return false; case ".lvb" or ".lgb" or ".sgb": - Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.", + Penumbra.Messager.NotificationMessage($"Redirection of {ext} files for {mod.Name} is unsupported as this breaks the game.\n\nThis mod will probably not work correctly.", NotificationType.Warning); return false; default: return true; From a73dee83b309104600f326d45e46f6fd9ab2180c Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 18 Feb 2025 14:12:54 +0000 Subject: [PATCH 588/865] [CI] Updating repo.json for testing_1.3.4.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index fdb0b638..c46f3d27 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.2", + "TestingAssemblyVersion": "1.3.4.3", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.3/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From ef26049c53d57cb5625f9dab476d5c61ae1c904b Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 5 Feb 2025 11:25:09 -0600 Subject: [PATCH 589/865] Consider VertexElement's UsageIndex Allows VertexDeclarations to have multiple VertexElements of the same Type but different UsageIndex --- Penumbra/Import/Models/Export/MeshExporter.cs | 117 ++++++++++++------ .../Import/Models/Export/VertexFragment.cs | 109 ++++++++++++++++ 2 files changed, 187 insertions(+), 39 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 73160615..0dc8a9ac 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -82,13 +82,16 @@ public class MeshExporter if (skeleton != null) _boneIndexMap = BuildBoneIndexMap(skeleton.Value); + var usages = new Dictionary>(); - var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements - .ToImmutableDictionary( - element => (MdlFile.VertexUsage)element.Usage, - element => (MdlFile.VertexType)element.Type - ); - + foreach (var element in _mdl.VertexDeclarations[_meshIndex].VertexElements) + { + if (!usages.ContainsKey((MdlFile.VertexUsage)element.Usage)) + { + usages.Add((MdlFile.VertexUsage)element.Usage, new Dictionary()); + } + usages[(MdlFile.VertexUsage)element.Usage][element.UsageIndex] = (MdlFile.VertexType)element.Type; + } _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); @@ -104,14 +107,20 @@ public class MeshExporter /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { + Penumbra.Log.Debug("Building Bone Index Map"); // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) + { + Penumbra.Log.Debug("BoneTableIndex was 255"); return null; + } var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); // #TODO @ackwell maybe fix for V6 Models, I think this works fine. + Penumbra.Log.Debug($"Version is 5 {_mdl.Version == MdlFile.V5}"); + foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { var boneName = _mdl.Bones[xivBoneIndex]; @@ -283,13 +292,20 @@ public class MeshExporter var vertices = new List(); - var attributes = new Dictionary(); + var attributes = new Dictionary>(); for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); - foreach (var (usage, element) in sortedElements) - attributes[usage] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); + { + if (!attributes.TryGetValue(usage, out var value)) + { + value = new Dictionary(); + attributes[usage] = value; + } + + value[element.UsageIndex] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); + } var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -320,7 +336,7 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary usages) + private Type GetGeometryType(IReadOnlyDictionary> usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw _notifier.Exception("Mesh does not contain position vertex elements."); @@ -335,28 +351,28 @@ public class MeshExporter } /// Build a geometry vertex from a vertex's attributes. - private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary attributes) + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position]) + ToVector3(attributes[MdlFile.VertexUsage.Position][0]) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]) + ToVector3(attributes[MdlFile.VertexUsage.Position][0]), + ToVector3(attributes[MdlFile.VertexUsage.Normal][0]) ); 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(attributes[MdlFile.VertexUsage.Tangent1]) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1][0]) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position]), - ToVector3(attributes[MdlFile.VertexUsage.Normal]), + ToVector3(attributes[MdlFile.VertexUsage.Position][0]), + ToVector3(attributes[MdlFile.VertexUsage.Normal][0]), bitangent ); } @@ -365,18 +381,23 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary usages) + private Type GetMaterialType(IReadOnlyDictionary> usages) { var uvCount = 0; - if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type)) - uvCount = type switch + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var dict)) + { + foreach (var type in dict.Values) { - MdlFile.VertexType.Half2 => 1, - MdlFile.VertexType.Half4 => 2, - MdlFile.VertexType.Single2 => 1, - MdlFile.VertexType.Single4 => 2, - _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), - }; + uvCount += type switch + { + MdlFile.VertexType.Half2 => 1, + MdlFile.VertexType.Half4 => 2, + MdlFile.VertexType.Single2 => 1, + MdlFile.VertexType.Single4 => 2, + _ => throw _notifier.Exception($"Unexpected UV vertex type {type}."), + }; + } + } var materialUsages = ( uvCount, @@ -385,6 +406,8 @@ public class MeshExporter return materialUsages switch { + (3, true) => typeof(VertexTexture3ColorFfxiv), + (3, false) => typeof(VertexTexture3), (2, true) => typeof(VertexTexture2ColorFfxiv), (2, false) => typeof(VertexTexture2), (1, true) => typeof(VertexTexture1ColorFfxiv), @@ -397,28 +420,28 @@ public class MeshExporter } /// Build a material vertex from a vertex's attributes. - private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary attributes) + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) { if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color])); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color][0])); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV])); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV][0])); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV]), - ToVector4(attributes[MdlFile.VertexUsage.Color]) + ToVector2(attributes[MdlFile.VertexUsage.UV][0]), + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -427,11 +450,27 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color]) + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ); + } + if (_materialType == typeof(VertexTexture3)) + { + throw _notifier.Exception("Unimplemented: Material Type is VertexTexture3"); + } + + if (_materialType == typeof(VertexTexture3ColorFfxiv)) + { + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + return new VertexTexture3ColorFfxiv( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y), + ToVector4(attributes[MdlFile.VertexUsage.Color][0]) ); } @@ -439,11 +478,11 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary usages) + private static Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights] == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights][0] == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -457,7 +496,7 @@ public class MeshExporter } /// Build a skinning vertex from a vertex's attributes. - private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary attributes) + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) { if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); @@ -467,8 +506,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices]; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights]; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices][0]; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights][0]; var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index eff34d54..c9b97997 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -282,3 +282,112 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } + +public struct VertexTexture3ColorFfxiv : 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("TEXCOORD_2", + new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false)); + yield return new KeyValuePair("_FFXIV_COLOR", + new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); + } + + public Vector2 TexCoord0; + public Vector2 TexCoord1; + public Vector2 TexCoord2; + public Vector4 FfxivColor; + + public int MaxColors + => 0; + + public int MaxTextCoords + => 3; + + private static readonly string[] CustomNames = ["_FFXIV_COLOR"]; + + public IEnumerable CustomAttributes + => CustomNames; + + public VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) + { + TexCoord0 = texCoord0; + TexCoord1 = texCoord1; + TexCoord2 = texCoord2; + FfxivColor = ffxivColor; + } + + 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": + value = FfxivColor; + return true; + + default: + value = null; + return false; + } + } + + public void SetCustomAttribute(string attributeName, object value) + { + if (attributeName == "_FFXIV_COLOR" && value is Vector4 valueVector4) + FfxivColor = valueVector4; + } + + public Vector4 GetColor(int index) + => throw new ArgumentOutOfRangeException(nameof(index)); + + public void SetColor(int setIndex, Vector4 color) + { } + + public void Validate() + { + var components = new[] + { + FfxivColor.X, + FfxivColor.Y, + FfxivColor.Z, + FfxivColor.W, + }; + if (components.Any(component => component < 0 || component > 1)) + throw new ArgumentOutOfRangeException(nameof(FfxivColor)); + } +} From 2f0bf19d00f9047817fb8bbfba38e5471f14bb20 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 09:48:22 -0600 Subject: [PATCH 590/865] Use First().Value --- Penumbra/Import/Models/Export/MeshExporter.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 0dc8a9ac..48f66177 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -355,24 +355,24 @@ public class MeshExporter { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]) + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]), - ToVector3(attributes[MdlFile.VertexUsage.Normal][0]) + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value) ); 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(attributes[MdlFile.VertexUsage.Tangent1][0]) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First().Value) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position][0]), - ToVector3(attributes[MdlFile.VertexUsage.Normal][0]), + ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value), bitangent ); } @@ -426,22 +426,22 @@ public class MeshExporter return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color][0])); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value)); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV][0])); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value)); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV][0]), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value), + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -450,11 +450,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); } if (_materialType == typeof(VertexTexture3)) @@ -470,7 +470,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color][0]) + ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) ); } @@ -482,7 +482,7 @@ public class MeshExporter { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights][0] == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights].First().Value == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -506,8 +506,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices][0]; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights][0]; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First().Value; + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First().Value; var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); From 579969a9e107e9a7b4b7adfef5f419cbed648899 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 12:45:15 -0600 Subject: [PATCH 591/865] Using LINQ And also change types from using LINQ --- Penumbra/Import/Models/Export/MeshExporter.cs | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 48f66177..fb88dfc3 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -82,16 +82,16 @@ public class MeshExporter if (skeleton != null) _boneIndexMap = BuildBoneIndexMap(skeleton.Value); - var usages = new Dictionary>(); - foreach (var element in _mdl.VertexDeclarations[_meshIndex].VertexElements) - { - if (!usages.ContainsKey((MdlFile.VertexUsage)element.Usage)) - { - usages.Add((MdlFile.VertexUsage)element.Usage, new Dictionary()); - } - usages[(MdlFile.VertexUsage)element.Usage][element.UsageIndex] = (MdlFile.VertexType)element.Type; - } + var usages = _mdl.VertexDeclarations[_meshIndex].VertexElements + .GroupBy(ele => (MdlFile.VertexUsage)ele.Usage, ele => ele) + .ToImmutableDictionary( + g => g.Key, + g => g.OrderBy(ele => ele.UsageIndex) // OrderBy UsageIndex is probably unnecessary as they're probably already be in order + .Select(ele => (MdlFile.VertexType)ele.Type) + .ToList() + ); + _geometryType = GetGeometryType(usages); _materialType = GetMaterialType(usages); _skinningType = GetSkinningType(usages); @@ -287,25 +287,22 @@ public class MeshExporter var sortedElements = _mdl.VertexDeclarations[_meshIndex].VertexElements .OrderBy(element => element.Offset) - .Select(element => ((MdlFile.VertexUsage)element.Usage, element)) .ToList(); - var vertices = new List(); - var attributes = new Dictionary>(); + var attributes = new Dictionary>(); for (var vertexIndex = 0; vertexIndex < XivMesh.VertexCount; vertexIndex++) { attributes.Clear(); - foreach (var (usage, element) in sortedElements) - { - if (!attributes.TryGetValue(usage, out var value)) - { - value = new Dictionary(); - attributes[usage] = value; - } - - value[element.UsageIndex] = ReadVertexAttribute((MdlFile.VertexType)element.Type, streams[element.Stream]); - } + attributes = sortedElements + .GroupBy(element => element.Usage) + .ToDictionary( + x => (MdlFile.VertexUsage)x.Key, + x => x.OrderBy(ele => ele.UsageIndex) // Once again, OrderBy UsageIndex is probably unnecessary + .Select(ele => ReadVertexAttribute((MdlFile.VertexType)ele.Type, streams[ele.Stream])) + .ToList() + ); + var vertexGeometry = BuildVertexGeometry(attributes); var vertexMaterial = BuildVertexMaterial(attributes); @@ -336,7 +333,7 @@ public class MeshExporter } /// Get the vertex geometry type for this mesh's vertex usages. - private Type GetGeometryType(IReadOnlyDictionary> usages) + private Type GetGeometryType(IReadOnlyDictionary> usages) { if (!usages.ContainsKey(MdlFile.VertexUsage.Position)) throw _notifier.Exception("Mesh does not contain position vertex elements."); @@ -351,28 +348,28 @@ public class MeshExporter } /// Build a geometry vertex from a vertex's attributes. - private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) + private IVertexGeometry BuildVertexGeometry(IReadOnlyDictionary> attributes) { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value) + ToVector3(attributes[MdlFile.VertexUsage.Position].First()) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value) + ToVector3(attributes[MdlFile.VertexUsage.Position].First()), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First()) ); 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(attributes[MdlFile.VertexUsage.Tangent1].First().Value) * 2 - Vector4.One; + var bitangent = ToVector4(attributes[MdlFile.VertexUsage.Tangent1].First()) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position].First().Value), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First().Value), + ToVector3(attributes[MdlFile.VertexUsage.Position].First()), + ToVector3(attributes[MdlFile.VertexUsage.Normal].First()), bitangent ); } @@ -381,12 +378,12 @@ public class MeshExporter } /// Get the vertex material type for this mesh's vertex usages. - private Type GetMaterialType(IReadOnlyDictionary> usages) + private Type GetMaterialType(IReadOnlyDictionary> usages) { var uvCount = 0; - if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var dict)) + if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var list)) { - foreach (var type in dict.Values) + foreach (var type in list) { uvCount += type switch { @@ -420,28 +417,28 @@ public class MeshExporter } /// Build a material vertex from a vertex's attributes. - private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) + private IVertexMaterial BuildVertexMaterial(IReadOnlyDictionary> attributes) { if (_materialType == typeof(VertexEmpty)) return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value)); + return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First())); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value)); + return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First())); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV].First().Value), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector2(attributes[MdlFile.VertexUsage.UV].First()), + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -450,11 +447,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First().Value); + var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); } if (_materialType == typeof(VertexTexture3)) @@ -470,7 +467,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color].First().Value) + ToVector4(attributes[MdlFile.VertexUsage.Color].First()) ); } @@ -478,11 +475,11 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary> usages) + private static Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights].First().Value == MdlFile.VertexType.UShort4) + if (usages[MdlFile.VertexUsage.BlendWeights].First() == MdlFile.VertexType.UShort4) { return typeof(VertexJoints8); } @@ -496,7 +493,7 @@ public class MeshExporter } /// Build a skinning vertex from a vertex's attributes. - private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) + private IVertexSkinning BuildVertexSkinning(IReadOnlyDictionary> attributes) { if (_skinningType == typeof(VertexEmpty)) return new VertexEmpty(); @@ -506,8 +503,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First().Value; - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First().Value; + var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First(); + var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First(); var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); From b76626ac8dbe9e4595be5e87a8f221415314ed18 Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 13:18:30 -0600 Subject: [PATCH 592/865] Added VertexTexture3 Not sure of accuracy but followed existing pattern --- Penumbra/Import/Models/Export/MeshExporter.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index fb88dfc3..a3cc2f04 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -456,7 +456,14 @@ public class MeshExporter } if (_materialType == typeof(VertexTexture3)) { - throw _notifier.Exception("Unimplemented: Material Type is VertexTexture3"); + // Not 100% sure about this + var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]); + var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]); + return new VertexTexture3( + new Vector2(uv0.X, uv0.Y), + new Vector2(uv0.Z, uv0.W), + new Vector2(uv1.X, uv1.Y) + ); } if (_materialType == typeof(VertexTexture3ColorFfxiv)) From 6d2b72e0798ac6cad82d54d9d0996ae666ff4c1e Mon Sep 17 00:00:00 2001 From: Adam Moy Date: Wed, 12 Feb 2025 13:30:06 -0600 Subject: [PATCH 593/865] Removed irrelevant comments --- Penumbra/Import/Models/Export/MeshExporter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index a3cc2f04..6d65e152 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -107,19 +107,14 @@ public class MeshExporter /// Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. private Dictionary? BuildBoneIndexMap(GltfSkeleton skeleton) { - Penumbra.Log.Debug("Building Bone Index Map"); // A BoneTableIndex of 255 means that this mesh is not skinned. if (XivMesh.BoneTableIndex == 255) - { - Penumbra.Log.Debug("BoneTableIndex was 255"); return null; - } var xivBoneTable = _mdl.BoneTables[XivMesh.BoneTableIndex]; var indexMap = new Dictionary(); // #TODO @ackwell maybe fix for V6 Models, I think this works fine. - Penumbra.Log.Debug($"Version is 5 {_mdl.Version == MdlFile.V5}"); foreach (var (xivBoneIndex, tableIndex) in xivBoneTable.BoneIndex.Take((int)xivBoneTable.BoneCount).WithIndex()) { From 31f23024a45b0663b87c97a1a8c0fe56b4b891b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:08 +0100 Subject: [PATCH 594/865] Notify and fail when a list of vertex usages has more than one entry where this is not expected. --- Penumbra/Import/Models/Export/MeshExporter.cs | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 6d65e152..aa0811d7 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -7,7 +7,6 @@ using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; -using SharpGLTF.IO; using SharpGLTF.Materials; using SharpGLTF.Scenes; @@ -347,24 +346,24 @@ public class MeshExporter { if (_geometryType == typeof(VertexPosition)) return new VertexPosition( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)) ); if (_geometryType == typeof(VertexPositionNormal)) return new VertexPositionNormal( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First()) + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), + 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(attributes[MdlFile.VertexUsage.Tangent1].First()) * 2 - Vector4.One; + var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One; return new VertexPositionNormalTangent( - ToVector3(attributes[MdlFile.VertexUsage.Position].First()), - ToVector3(attributes[MdlFile.VertexUsage.Normal].First()), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)), + ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)), bitangent ); } @@ -418,22 +417,22 @@ public class MeshExporter return new VertexEmpty(); if (_materialType == typeof(VertexColorFfxiv)) - return new VertexColorFfxiv(ToVector4(attributes[MdlFile.VertexUsage.Color].First())); + return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))); if (_materialType == typeof(VertexTexture1)) - return new VertexTexture1(ToVector2(attributes[MdlFile.VertexUsage.UV].First())); + return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV))); if (_materialType == typeof(VertexTexture1ColorFfxiv)) return new VertexTexture1ColorFfxiv( - ToVector2(attributes[MdlFile.VertexUsage.UV].First()), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)), + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); // XIV packs two UVs into a single vec4 attribute. if (_materialType == typeof(VertexTexture2)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); return new VertexTexture2( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W) @@ -442,11 +441,11 @@ public class MeshExporter if (_materialType == typeof(VertexTexture2ColorFfxiv)) { - var uv = ToVector4(attributes[MdlFile.VertexUsage.UV].First()); + var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)); return new VertexTexture2ColorFfxiv( new Vector2(uv.X, uv.Y), new Vector2(uv.Z, uv.W), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } if (_materialType == typeof(VertexTexture3)) @@ -469,7 +468,7 @@ public class MeshExporter new Vector2(uv0.X, uv0.Y), new Vector2(uv0.Z, uv0.W), new Vector2(uv1.X, uv1.Y), - ToVector4(attributes[MdlFile.VertexUsage.Color].First()) + ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)) ); } @@ -477,18 +476,13 @@ public class MeshExporter } /// Get the vertex skinning type for this mesh's vertex usages. - private static Type GetSkinningType(IReadOnlyDictionary> usages) + private Type GetSkinningType(IReadOnlyDictionary> usages) { if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices)) { - if (usages[MdlFile.VertexUsage.BlendWeights].First() == MdlFile.VertexType.UShort4) - { - return typeof(VertexJoints8); - } - else - { - return typeof(VertexJoints4); - } + return GetFirstSafe(usages, MdlFile.VertexUsage.BlendWeights) == MdlFile.VertexType.UShort4 + ? typeof(VertexJoints8) + : typeof(VertexJoints4); } return typeof(VertexEmpty); @@ -505,8 +499,8 @@ public class MeshExporter if (_boneIndexMap == null) throw _notifier.Exception("Tried to build skinned vertex but no bone mappings are available."); - var indiciesData = attributes[MdlFile.VertexUsage.BlendIndices].First(); - var weightsData = attributes[MdlFile.VertexUsage.BlendWeights].First(); + var indiciesData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendIndices); + var weightsData = GetFirstSafe(attributes, MdlFile.VertexUsage.BlendWeights); var indices = ToByteArray(indiciesData); var weights = ToFloatArray(weightsData); @@ -533,6 +527,17 @@ public class MeshExporter throw _notifier.Exception($"Unknown skinning type {_skinningType}"); } + /// Check that the list has length 1 for any case where this is expected and return the one entry. + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private T GetFirstSafe(IReadOnlyDictionary> attributes, MdlFile.VertexUsage usage) + { + var list = attributes[usage]; + if (list.Count != 1) + throw _notifier.Exception($"Multiple usage indices encountered for {usage}."); + + return list[0]; + } + /// Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. private static Vector2 ToVector2(object data) => data switch From f8d0616acd835eefc394b7303b6e4ee55f1e923d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:33 +0100 Subject: [PATCH 595/865] Notify when an unhandled UV count is reached. --- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index aa0811d7..32b9b323 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -406,7 +406,7 @@ public class MeshExporter (0, true) => typeof(VertexColorFfxiv), (0, false) => typeof(VertexEmpty), - _ => throw new Exception("Unreachable."), + _ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."), }; } From d40c59eee98bcd2f20ac0ce701e181bf6eb7bf70 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:36:46 +0100 Subject: [PATCH 596/865] Slight cleanup. --- .../Import/Models/Export/VertexFragment.cs | 56 ++++++------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/Penumbra/Import/Models/Export/VertexFragment.cs b/Penumbra/Import/Models/Export/VertexFragment.cs index c9b97997..56495f2f 100644 --- a/Penumbra/Import/Models/Export/VertexFragment.cs +++ b/Penumbra/Import/Models/Export/VertexFragment.cs @@ -1,4 +1,3 @@ -using System; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Memory; using SharpGLTF.Schema2; @@ -11,7 +10,7 @@ Realistically, it will need to stick around until transforms/mutations are built and there's reason to overhaul the export pipeline. */ -public struct VertexColorFfxiv : IVertexCustom +public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -20,7 +19,7 @@ public struct VertexColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector4 FfxivColor; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -33,9 +32,6 @@ public struct VertexColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexColorFfxiv(Vector4 ffxivColor) - => FfxivColor = ffxivColor; - public void Add(in VertexMaterialDelta delta) { } @@ -88,7 +84,7 @@ public struct VertexColorFfxiv : IVertexCustom } } -public struct VertexTexture1ColorFfxiv : IVertexCustom +public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -98,9 +94,9 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; + public Vector2 TexCoord0 = texCoord0; - public Vector4 FfxivColor; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -113,12 +109,6 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -182,7 +172,7 @@ public struct VertexTexture1ColorFfxiv : IVertexCustom } } -public struct VertexTexture2ColorFfxiv : IVertexCustom +public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -194,9 +184,9 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; - public Vector2 TexCoord1; - public Vector4 FfxivColor; + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -209,13 +199,6 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -283,7 +266,8 @@ public struct VertexTexture2ColorFfxiv : IVertexCustom } } -public struct VertexTexture3ColorFfxiv : IVertexCustom +public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) + : IVertexCustom { public IEnumerable> GetEncodingAttributes() { @@ -297,10 +281,10 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true)); } - public Vector2 TexCoord0; - public Vector2 TexCoord1; - public Vector2 TexCoord2; - public Vector4 FfxivColor; + public Vector2 TexCoord0 = texCoord0; + public Vector2 TexCoord1 = texCoord1; + public Vector2 TexCoord2 = texCoord2; + public Vector4 FfxivColor = ffxivColor; public int MaxColors => 0; @@ -313,14 +297,6 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom public IEnumerable CustomAttributes => CustomNames; - public VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor) - { - TexCoord0 = texCoord0; - TexCoord1 = texCoord1; - TexCoord2 = texCoord2; - FfxivColor = ffxivColor; - } - public void Add(in VertexMaterialDelta delta) { TexCoord0 += delta.TexCoord0Delta; @@ -387,7 +363,7 @@ public struct VertexTexture3ColorFfxiv : IVertexCustom FfxivColor.Z, FfxivColor.W, }; - if (components.Any(component => component < 0 || component > 1)) + if (components.Any(component => component is < 0f or > 1f)) throw new ArgumentOutOfRangeException(nameof(FfxivColor)); } } From 1f172b463206bc41b5769f0d6148d551aefdda50 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 18:37:15 +0100 Subject: [PATCH 597/865] Make default constructed models use V6 instead of V5. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f6dff467..b4a0806e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f6dff467c7dad6b1213a7d7b65d40a56450f0672 +Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7 From fdd75e2866a10aa380eddf46d615af6ad373f11a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 20 Feb 2025 19:17:55 +0100 Subject: [PATCH 598/865] Use Meta Compression V1. --- Penumbra/Api/Api/MetaApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index ff88ae4e..7c0cd5fc 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -51,7 +51,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } internal static string CompressMetaManipulations(ModCollection collection) - => CompressMetaManipulationsV0(collection); + => CompressMetaManipulationsV1(collection); private static string CompressMetaManipulationsV0(ModCollection collection) { From 4a00d82921468397ed7262ff8aef470090d9d398 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 20 Feb 2025 18:20:23 +0000 Subject: [PATCH 599/865] [CI] Updating repo.json for testing_1.3.4.4 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c46f3d27..afb2c32d 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.3", + "TestingAssemblyVersion": "1.3.4.4", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.4/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 514b0e7f30f4a792726a1bca9c811bdf0a373ee2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 27 Feb 2025 00:10:24 +0100 Subject: [PATCH 600/865] Add file types to Resource Tree and require Ctrl+Shift for some quick imports --- Penumbra.GameData | 2 +- .../ResolveContext.PathResolution.cs | 84 ++++++++++++----- .../Interop/ResourceTree/ResolveContext.cs | 93 ++++++++++++++++++- Penumbra/Interop/ResourceTree/ResourceNode.cs | 11 ++- Penumbra/Interop/ResourceTree/ResourceTree.cs | 50 ++++++++-- Penumbra/Interop/Structs/StructExtensions.cs | 12 +++ .../ModEditWindow.QuickImport.cs | 5 +- 7 files changed, 224 insertions(+), 33 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b4a0806e..c59b1da6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b4a0806e00be4ce8cf3103fd526e4a412b4770b7 +Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7 diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index bdf66a16..cd6b8568 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -22,6 +22,13 @@ internal partial record ResolveContext private static bool IsEquipmentSlot(uint slotIndex) => slotIndex is < 5 or 16 or 17; + private unsafe Variant Variant + => ModelType switch + { + ModelType.Monster => (byte)((Monster*)CharacterBase)->Variant, + _ => Equipment.Variant, + }; + private Utf8GamePath ResolveModelPath() { // Correctness: @@ -92,7 +99,7 @@ internal partial record ResolveContext => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.DemiHuman => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), ModelType.Weapon => ResolveWeaponMaterialPath(modelPath, imc, mtrlFileName), - ModelType.Monster => ResolveMonsterMaterialPath(modelPath, imc, mtrlFileName), + ModelType.Monster => ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName), _ => ResolveMaterialPathNative(mtrlFileName), }; } @@ -100,7 +107,7 @@ internal partial record ResolveContext [SkipLocalsInit] private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var variant = ResolveImcData(imc).MaterialId; var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; @@ -118,9 +125,9 @@ internal partial record ResolveContext return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; // Some offhands share materials with the corresponding mainhand - if (ItemData.AdaptOffhandImc(Equipment.Set.Id, out var mirroredSetId)) + if (ItemData.AdaptOffhandImc(Equipment.Set, out var mirroredSetId)) { - var variant = ResolveMaterialVariant(imc, Equipment.Variant); + var variant = ResolveImcData(imc).MaterialId; var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); Span mirroredFileName = stackalloc byte[32]; @@ -141,31 +148,16 @@ internal partial record ResolveContext return ResolveEquipmentMaterialPath(modelPath, imc, mtrlFileName); } - private unsafe Utf8GamePath ResolveMonsterMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName) - { - var variant = ResolveMaterialVariant(imc, (byte)((Monster*)CharacterBase)->Variant); - var fileName = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(mtrlFileName); - - Span pathBuffer = stackalloc byte[CharaBase.PathBufferSize]; - pathBuffer = AssembleMaterialPath(pathBuffer, modelPath.Path.Span, variant, fileName); - - return Utf8GamePath.FromSpan(pathBuffer, MetaDataComputation.None, out var path) ? path.Clone() : Utf8GamePath.Empty; - } - - private unsafe byte ResolveMaterialVariant(ResourceHandle* imc, Variant variant) + private unsafe ImcEntry ResolveImcData(ResourceHandle* imc) { var imcFileData = imc->GetDataSpan(); if (imcFileData.IsEmpty) { Penumbra.Log.Warning($"IMC resource handle with path {imc->FileName.AsByteString()} doesn't have a valid data span"); - return variant.Id; + return default; } - var entry = ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), variant, out var exists); - if (!exists) - return variant.Id; - - return entry.MaterialId; + return ImcFile.GetEntry(imcFileData, SlotIndex.ToEquipSlot(), Variant, out _); } private static Span AssembleMaterialPath(Span materialPathBuffer, ReadOnlySpan modelPath, byte variant, @@ -317,4 +309,52 @@ internal partial record ResolveContext var path = CharacterBase->ResolveSkpPathAsByteString(partialSkeletonIndex); return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } + + private Utf8GamePath ResolvePhysicsModulePath(uint partialSkeletonIndex) + { + // Correctness and Safety: + // Resolving a physics 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 => ResolveHumanPhysicsModulePath(partialSkeletonIndex), + _ => ResolvePhysicsModulePathNative(partialSkeletonIndex), + }; + } + + private Utf8GamePath ResolveHumanPhysicsModulePath(uint partialSkeletonIndex) + { + var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); + if (set == 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolvePhysicsModulePathNative(uint partialSkeletonIndex) + { + var path = CharacterBase->ResolvePhybPathAsByteString(partialSkeletonIndex); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) + { + var animation = ResolveImcData(imc).MaterialAnimationId; + if (animation == 0) + return Utf8GamePath.Empty; + + var path = CharacterBase->ResolveMaterialPapPathAsByteString(SlotIndex, animation); + return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } + + private unsafe Utf8GamePath ResolveDecalPath(ResourceHandle* imc) + { + var decal = ResolveImcData(imc).DecalId; + if (decal == 0) + return Utf8GamePath.Empty; + + var path = GamePaths.Equipment.Decal.Path(decal); + return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; + } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 54612070..f33bf041 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -180,7 +180,15 @@ internal unsafe partial record ResolveContext( return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, path); } - public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc) + public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, Utf8GamePath gamePath) + { + if (tex == null) + return null; + + return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); + } + + public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) { if (mdl == null || mdl->ModelResourceHandle == null) return null; @@ -210,6 +218,14 @@ internal unsafe partial record ResolveContext( } } + var decalNode = CreateNodeFromDecal(decalHandle, imc); + if (null != decalNode) + node.Children.Add(decalNode); + + var mpapNode = CreateNodeFromMaterialPap(mpapHandle, imc); + if (null != mpapNode) + node.Children.Add(mpapNode); + Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); return node; @@ -301,7 +317,59 @@ internal unsafe partial record ResolveContext( } } - public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) + public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc) + { + if (decalHandle == null) + return null; + + var path = ResolveDecalPath(imc); + if (path.IsEmpty) + return null; + + var node = CreateNodeFromTex(decalHandle, path)!; + if (Global.WithUiData) + node.FallbackName = "Decal"; + + return node; + } + + public ResourceNode? CreateNodeFromMaterialPap(ResourceHandle* mpapHandle, ResourceHandle* imc) + { + if (mpapHandle == null) + return null; + + var path = ResolveMaterialAnimationPath(imc); + if (path.IsEmpty) + return null; + + if (Global.Nodes.TryGetValue((path, (nint)mpapHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Pap, 0, mpapHandle, path); + if (Global.WithUiData) + node.FallbackName = "Material Animation"; + + return node; + } + + public ResourceNode? CreateNodeFromMaterialSklb(SkeletonResourceHandle* sklbHandle) + { + if (sklbHandle == null) + return null; + + if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path)) + return null; + + if (Global.Nodes.TryGetValue((path, (nint)sklbHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, path); + node.ForceInternal = true; + + return node; + } + + public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) { if (sklb == null || sklb->SkeletonResourceHandle == null) return null; @@ -315,6 +383,9 @@ internal unsafe partial record ResolveContext( var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); if (skpNode != null) node.Children.Add(skpNode); + var phybNode = CreateNodeFromPhyb(phybHandle, partialSkeletonIndex); + if (phybNode != null) + node.Children.Add(phybNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); return node; @@ -338,6 +409,24 @@ internal unsafe partial record ResolveContext( return node; } + private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex) + { + if (phybHandle == null) + return null; + + var path = ResolvePhysicsModulePath(partialSkeletonIndex); + + if (Global.Nodes.TryGetValue((path, (nint)phybHandle), out var cached)) + return cached; + + var node = CreateNode(ResourceType.Phyb, 0, phybHandle, path, false); + if (Global.WithUiData) + node.FallbackName = "Physics Module"; + Global.Nodes.Add((path, (nint)phybHandle), 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 60cc48de..24cb8f02 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -21,6 +21,8 @@ public class ResourceNode : ICloneable public readonly WeakReference Mod = new(null!); public string? ModRelativePath; public CiByteString AdditionalData; + public bool ForceInternal; + public bool ForceProtected; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; @@ -37,8 +39,13 @@ public class ResourceNode : ICloneable } } + /// Whether to treat the file as internal (hide from user unless debug mode is on). public bool Internal - => Type is ResourceType.Eid or ResourceType.Imc; + => ForceInternal || Type is ResourceType.Eid or ResourceType.Imc; + + /// 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; internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext) { @@ -67,6 +74,8 @@ public class ResourceNode : ICloneable Mod = other.Mod; ModRelativePath = other.ModRelativePath; AdditionalData = other.AdditionalData; + ForceInternal = other.ForceInternal; + ForceProtected = other.ForceProtected; Length = other.Length; Children = other.Children; ResolveContext = other.ResolveContext; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index b50fc695..ac1f889c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -1,7 +1,9 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Graphics.Physics; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -10,6 +12,7 @@ using Penumbra.UI; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; +using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle; namespace Penumbra.Interop.ResourceTree; @@ -74,6 +77,18 @@ public class ResourceTree var genericContext = globalContext.CreateContext(model); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); + var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; + var decalArray = modelType switch + { + ModelType.Human => human->SlotDecalsSpan, + ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals, + ModelType.Weapon => [((Weapon*)model)->Decal], + ModelType.Monster => [((Monster*)model)->Decal], + _ => [], + }; + for (var i = 0u; i < model->SlotCount; ++i) { var slotContext = modelType switch @@ -100,7 +115,8 @@ public class ResourceTree } var mdl = model->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, + i < mpapArray.Length ? mpapArray[(int)i].Value : null); if (mdlNode != null) { if (globalContext.WithUiData) @@ -109,7 +125,9 @@ public class ResourceTree } } - AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton); + AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940)); AddWeapons(globalContext, model); @@ -140,6 +158,10 @@ 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 mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; + for (var i = 0; i < subObject->SlotCount; ++i) { var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType); @@ -154,7 +176,7 @@ public class ResourceTree } var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc); + var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null); if (mdlNode != null) { if (globalContext.WithUiData) @@ -163,7 +185,9 @@ public class ResourceTree } } - AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, $"Weapon #{weaponIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, "); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), $"Weapon #{weaponIndex}, "); ++weaponIndex; } @@ -216,6 +240,7 @@ public class ResourceTree var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath); if (legacyDecalNode != null) { + legacyDecalNode.ForceProtected = !hasLegacyDecal; if (globalContext.WithUiData) { legacyDecalNode = legacyDecalNode.Clone(); @@ -227,7 +252,7 @@ public class ResourceTree } } - private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, string prefix = "") + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) @@ -242,7 +267,9 @@ public class ResourceTree for (var i = 0; i < skeleton->PartialSkeletonCount; ++i) { - var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], (uint)i); + // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) + var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; + var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i); if (sklbNode != null) { if (context.Global.WithUiData) @@ -251,4 +278,15 @@ public class ResourceTree } } } + + private unsafe void AddMaterialAnimationSkeleton(List nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, string prefix = "") + { + var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle); + if (sklbNode == null) + return; + + if (context.Global.WithUiData) + sklbNode.FallbackName = $"{prefix}Material Animation Skeleton"; + nodes.Add(sklbNode); + } } diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 9dd9a96d..8b5974f0 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -33,6 +33,12 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName)); } + public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId)); + } + public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) { Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; @@ -45,6 +51,12 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex)); } + public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex) + { + Span pathBuffer = stackalloc byte[CharacterBase.PathBufferSize]; + return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); + } + private static unsafe CiByteString ToOwnedByteString(byte* str) => str == null ? CiByteString.Empty : new CiByteString(str).Clone(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 6fb223df..a49d2933 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -110,8 +110,11 @@ public partial class ModEditWindow _quickImportActions.Add((resourceNode.GamePath, writable), quickImport); } + var canQuickImport = quickImport.CanExecute; + var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive()); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, - $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true)) + $"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}", + !quickImportEnabled, true)) { quickImport.Execute(); _quickImportActions.Remove((resourceNode.GamePath, writable)); From 776a93dc73b03f87dbcea4165f4ab571f4f21b0c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:38:42 +0100 Subject: [PATCH 601/865] Some null-check cleanup. --- .../ResolveContext.PathResolution.cs | 10 +-- .../Interop/ResourceTree/ResolveContext.cs | 81 ++++++++----------- 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index cd6b8568..b1ca24b0 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -248,7 +248,7 @@ internal partial record ResolveContext if (faceId < 201) faceId -= tribe switch { - 0xB when modelType == 4 => 100, + 0xB when modelType is 4 => 100, 0xE | 0xF => 100, _ => 0, }; @@ -297,7 +297,7 @@ internal partial record ResolveContext private Utf8GamePath ResolveHumanSkeletonParameterPath(uint partialSkeletonIndex) { var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); - if (set == 0) + if (set.Id is 0) return Utf8GamePath.Empty; var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set); @@ -325,7 +325,7 @@ internal partial record ResolveContext private Utf8GamePath ResolveHumanPhysicsModulePath(uint partialSkeletonIndex) { var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex); - if (set == 0) + if (set.Id is 0) return Utf8GamePath.Empty; var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set); @@ -341,7 +341,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc) { var animation = ResolveImcData(imc).MaterialAnimationId; - if (animation == 0) + if (animation is 0) return Utf8GamePath.Empty; var path = CharacterBase->ResolveMaterialPapPathAsByteString(SlotIndex, animation); @@ -351,7 +351,7 @@ internal partial record ResolveContext private unsafe Utf8GamePath ResolveDecalPath(ResourceHandle* imc) { var decal = ResolveImcData(imc).DecalId; - if (decal == 0) + if (decal is 0) return Utf8GamePath.Empty; var path = GamePaths.Equipment.Decal.Path(decal); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index f33bf041..81904819 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -52,7 +52,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateNodeFromShpk(ShaderPackageResourceHandle* resourceHandle, CiByteString gamePath) { - if (resourceHandle == null) + if (resourceHandle is null) return null; if (gamePath.IsEmpty) return null; @@ -65,7 +65,7 @@ internal unsafe partial record ResolveContext( [SkipLocalsInit] private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, CiByteString gamePath, bool dx11) { - if (resourceHandle == null) + if (resourceHandle is null) return null; Utf8GamePath path; @@ -105,7 +105,7 @@ internal unsafe partial record ResolveContext( private ResourceNode GetOrCreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath) { - if (resourceHandle == null) + if (resourceHandle is null) throw new ArgumentNullException(nameof(resourceHandle)); if (Global.Nodes.TryGetValue((gamePath, (nint)resourceHandle), out var cached)) @@ -117,7 +117,7 @@ internal unsafe partial record ResolveContext( private ResourceNode CreateNode(ResourceType type, nint objectAddress, ResourceHandle* resourceHandle, Utf8GamePath gamePath, bool autoAdd = true) { - if (resourceHandle == null) + if (resourceHandle is null) throw new ArgumentNullException(nameof(resourceHandle)); var fileName = (ReadOnlySpan)resourceHandle->FileName.AsSpan(); @@ -141,7 +141,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromEid(ResourceHandle* eid) { - if (eid == null) + if (eid is null) return null; if (!Utf8GamePath.FromByteString(CharacterBase->ResolveEidPathAsByteString(), out var path)) @@ -152,7 +152,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromImc(ResourceHandle* imc) { - if (imc == null) + if (imc is null) return null; if (!Utf8GamePath.FromByteString(CharacterBase->ResolveImcPathAsByteString(SlotIndex), out var path)) @@ -163,7 +163,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromPbd(ResourceHandle* pbd) { - if (pbd == null) + if (pbd is null) return null; return GetOrCreateNode(ResourceType.Pbd, 0, pbd, PreBoneDeformerReplacer.PreBoneDeformerPath); @@ -171,7 +171,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath) { - if (tex == null) + if (tex is null) return null; if (!Utf8GamePath.FromString(gamePath, out var path)) @@ -182,7 +182,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, Utf8GamePath gamePath) { - if (tex == null) + if (tex is null) return null; return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath); @@ -190,7 +190,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle) { - if (mdl == null || mdl->ModelResourceHandle == null) + if (mdl is null || mdl->ModelResourceHandle is null) return null; var mdlResource = mdl->ModelResourceHandle; @@ -205,12 +205,12 @@ internal unsafe partial record ResolveContext( for (var i = 0; i < mdl->MaterialCount; i++) { var mtrl = mdl->Materials[i]; - if (mtrl == null) + if (mtrl is null) continue; var mtrlFileName = mdlResource->GetMaterialFileNameBySlot((uint)i); var mtrlNode = CreateNodeFromMaterial(mtrl, ResolveMaterialPath(path, imc, mtrlFileName)); - if (mtrlNode != null) + if (mtrlNode is not null) { if (Global.WithUiData) mtrlNode.FallbackName = $"Material #{i}"; @@ -218,12 +218,10 @@ internal unsafe partial record ResolveContext( } } - var decalNode = CreateNodeFromDecal(decalHandle, imc); - if (null != decalNode) + if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode) node.Children.Add(decalNode); - var mpapNode = CreateNodeFromMaterialPap(mpapHandle, imc); - if (null != mpapNode) + if (CreateNodeFromMaterialPap(mpapHandle, imc) is { } mpapNode) node.Children.Add(mpapNode); Global.Nodes.Add((path, (nint)mdl->ModelResourceHandle), node); @@ -233,7 +231,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateNodeFromMaterial(Material* mtrl, Utf8GamePath path) { - if (mtrl == null || mtrl->MaterialResourceHandle == null) + if (mtrl is null || mtrl->MaterialResourceHandle is null) return null; var resource = mtrl->MaterialResourceHandle; @@ -242,15 +240,15 @@ internal unsafe partial record ResolveContext( var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName)); - if (shpkNode != null) + if (shpkNode is not null) { if (Global.WithUiData) shpkNode.Name = "Shader Package"; node.Children.Add(shpkNode); } - var shpkNames = Global.WithUiData && shpkNode != null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; - var shpk = Global.WithUiData && shpkNode != null ? (ShaderPackage*)shpkNode.ObjectAddress : null; + var shpkNames = Global.WithUiData && shpkNode is not null ? Global.TreeBuildCache.ReadShaderPackageNames(shpkNode.FullPath) : null; + var shpk = Global.WithUiData && shpkNode is not null ? (ShaderPackage*)shpkNode.ObjectAddress : null; var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) @@ -263,7 +261,7 @@ internal unsafe partial record ResolveContext( if (Global.WithUiData) { string? name = null; - if (shpk != null) + if (shpk is not null) { var index = GetTextureIndex(mtrl, resource->Textures[i].Flags, alreadyProcessedSamplerIds); var samplerId = index != 0x001F @@ -319,7 +317,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromDecal(TextureResourceHandle* decalHandle, ResourceHandle* imc) { - if (decalHandle == null) + if (decalHandle is null) return null; var path = ResolveDecalPath(imc); @@ -335,7 +333,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromMaterialPap(ResourceHandle* mpapHandle, ResourceHandle* imc) { - if (mpapHandle == null) + if (mpapHandle is null) return null; var path = ResolveMaterialAnimationPath(imc); @@ -354,7 +352,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromMaterialSklb(SkeletonResourceHandle* sklbHandle) { - if (sklbHandle == null) + if (sklbHandle is null) return null; if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path)) @@ -371,7 +369,7 @@ internal unsafe partial record ResolveContext( public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex) { - if (sklb == null || sklb->SkeletonResourceHandle == null) + if (sklb is null || sklb->SkeletonResourceHandle is null) return null; var path = ResolveSkeletonPath(partialSkeletonIndex); @@ -379,12 +377,10 @@ internal unsafe partial record ResolveContext( if (Global.Nodes.TryGetValue((path, (nint)sklb->SkeletonResourceHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); - var skpNode = CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex); - if (skpNode != null) + var node = CreateNode(ResourceType.Sklb, (nint)sklb, (ResourceHandle*)sklb->SkeletonResourceHandle, path, false); + if (CreateParameterNodeFromPartialSkeleton(sklb, partialSkeletonIndex) is { } skpNode) node.Children.Add(skpNode); - var phybNode = CreateNodeFromPhyb(phybHandle, partialSkeletonIndex); - if (phybNode != null) + if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode) node.Children.Add(phybNode); Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node); @@ -393,7 +389,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateParameterNodeFromPartialSkeleton(PartialSkeleton* sklb, uint partialSkeletonIndex) { - if (sklb == null || sklb->SkeletonParameterResourceHandle == null) + if (sklb is null || sklb->SkeletonParameterResourceHandle is null) return null; var path = ResolveSkeletonParameterPath(partialSkeletonIndex); @@ -411,7 +407,7 @@ internal unsafe partial record ResolveContext( private ResourceNode? CreateNodeFromPhyb(ResourceHandle* phybHandle, uint partialSkeletonIndex) { - if (phybHandle == null) + if (phybHandle is null) return null; var path = ResolvePhysicsModulePath(partialSkeletonIndex); @@ -431,7 +427,9 @@ internal unsafe partial record ResolveContext( { var path = gamePath.Path.Split((byte)'/'); // Weapons intentionally left out. - var isEquipment = path.Count >= 2 && path[0].Span.SequenceEqual("chara"u8) && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); + var isEquipment = path.Count >= 2 + && path[0].Span.SequenceEqual("chara"u8) + && (path[1].Span.SequenceEqual("accessory"u8) || path[1].Span.SequenceEqual("equipment"u8)); if (isEquipment) foreach (var item in Global.Identifier.Identify(Equipment.Set, 0, Equipment.Variant, Slot.ToSlot())) { @@ -447,7 +445,7 @@ internal unsafe partial record ResolveContext( } var dataFromPath = GuessUiDataFromPath(gamePath); - if (dataFromPath.Name != null) + if (dataFromPath.Name is not null) return dataFromPath; return isEquipment @@ -462,24 +460,13 @@ internal unsafe partial record ResolveContext( var name = obj.Key; if (obj.Value is IdentifiedCustomization) name = name[14..].Trim(); - if (name != "Unknown") + if (name is not "Unknown") return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } return new ResourceNode.UiData(null, ChangedItemIconFlag.Unknown); } - private static string? SafeGet(ReadOnlySpan array, Index index) - { - var i = index.GetOffset(array.Length); - return i >= 0 && i < array.Length ? array[i] : null; - } - private static ulong GetResourceHandleLength(ResourceHandle* handle) - { - if (handle == null) - return 0; - - return handle->GetLength(); - } + => handle is null ? 0ul : handle->GetLength(); } From e4cfd674ee1443f876574fa5f8e351413d739d7d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:39:19 +0100 Subject: [PATCH 602/865] Probably unnecessary size optimization. --- Penumbra/Interop/ResourceTree/ResourceNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 24cb8f02..3699ae0b 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -17,12 +17,12 @@ public class ResourceNode : ICloneable public Utf8GamePath[] PossibleGamePaths; public FullPath FullPath; public PathStatus FullPathStatus; + public bool ForceInternal; + public bool ForceProtected; public string? ModName; public readonly WeakReference Mod = new(null!); public string? ModRelativePath; public CiByteString AdditionalData; - public bool ForceInternal; - public bool ForceProtected; public readonly ulong Length; public readonly List Children; internal ResolveContext? ResolveContext; From 70844610d8a913ffd915d0348882cb14bd50a89f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:45:06 +0100 Subject: [PATCH 603/865] Primary constructor and some null-check cleanup. --- Penumbra/Interop/ResourceTree/ResourceTree.cs | 132 ++++++++---------- 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index ac1f889c..5e3f52d4 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -16,42 +16,35 @@ using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.Re namespace Penumbra.Interop.ResourceTree; -public class ResourceTree +public class ResourceTree( + string name, + string anonymizedName, + int gameObjectIndex, + nint gameObjectAddress, + nint drawObjectAddress, + bool localPlayerRelated, + bool playerRelated, + bool networked, + string collectionName, + string anonymizedCollectionName) { - public readonly string Name; - public readonly string AnonymizedName; - public readonly int GameObjectIndex; - public readonly nint GameObjectAddress; - public readonly nint DrawObjectAddress; - public readonly bool LocalPlayerRelated; - public readonly bool PlayerRelated; - public readonly bool Networked; - public readonly string CollectionName; - public readonly string AnonymizedCollectionName; - public readonly List Nodes; - public readonly HashSet FlatNodes; + public readonly string Name = name; + public readonly string AnonymizedName = anonymizedName; + public readonly int GameObjectIndex = gameObjectIndex; + public readonly nint GameObjectAddress = gameObjectAddress; + public readonly nint DrawObjectAddress = drawObjectAddress; + public readonly bool LocalPlayerRelated = localPlayerRelated; + public readonly bool PlayerRelated = playerRelated; + public readonly bool Networked = networked; + public readonly string CollectionName = collectionName; + public readonly string AnonymizedCollectionName = anonymizedCollectionName; + public readonly List Nodes = []; + public readonly HashSet FlatNodes = []; public int ModelId; public CustomizeData CustomizeData; public GenderRace RaceCode; - public ResourceTree(string name, string anonymizedName, int gameObjectIndex, nint gameObjectAddress, nint drawObjectAddress, - bool localPlayerRelated, bool playerRelated, bool networked, string collectionName, string anonymizedCollectionName) - { - Name = name; - AnonymizedName = anonymizedName; - GameObjectIndex = gameObjectIndex; - GameObjectAddress = gameObjectAddress; - DrawObjectAddress = drawObjectAddress; - LocalPlayerRelated = localPlayerRelated; - Networked = networked; - PlayerRelated = playerRelated; - CollectionName = collectionName; - AnonymizedCollectionName = anonymizedCollectionName; - Nodes = []; - FlatNodes = []; - } - public void ProcessPostfix(Action action) { foreach (var node in Nodes) @@ -73,13 +66,13 @@ public class ResourceTree }; ModelId = character->ModelContainer.ModelCharaId; CustomizeData = character->DrawData.CustomizeData; - RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; + RaceCode = human is not null ? (GenderRace)human->RaceSexId : GenderRace.Unknown; var genericContext = globalContext.CreateContext(model); // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948); - var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; + var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, model->SlotCount) : []; var decalArray = modelType switch { ModelType.Human => human->SlotDecalsSpan, @@ -105,19 +98,17 @@ public class ResourceTree : globalContext.CreateContext(model, i), }; - var imc = (ResourceHandle*)model->IMCArray[i]; - var imcNode = slotContext.CreateNodeFromImc(imc); - if (imcNode != null) + var imc = (ResourceHandle*)model->IMCArray[i]; + if (slotContext.CreateNodeFromImc(imc) is { } imcNode) { if (globalContext.WithUiData) imcNode.FallbackName = $"IMC #{i}"; Nodes.Add(imcNode); } - var mdl = model->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null, - i < mpapArray.Length ? mpapArray[(int)i].Value : null); - if (mdlNode != null) + 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) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Model #{i}"; @@ -131,7 +122,7 @@ public class ResourceTree AddWeapons(globalContext, model); - if (human != null) + if (human is not null) AddHumanResources(globalContext, human); } @@ -141,12 +132,12 @@ public class ResourceTree var weaponNodes = new List(); foreach (var baseSubObject in model->DrawObject.Object.ChildObjects) { - if (baseSubObject->GetObjectType() != FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) + if (baseSubObject->GetObjectType() is not FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType.CharacterBase) continue; var subObject = (CharacterBase*)baseSubObject; - if (subObject->GetModelType() != ModelType.Weapon) + if (subObject->GetModelType() is not ModelType.Weapon) continue; var weapon = (Weapon*)subObject; @@ -160,24 +151,22 @@ public class ResourceTree // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948); - var mpapArray = null != mpapArrayPtr ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; + var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan>(mpapArrayPtr, subObject->SlotCount) : []; for (var i = 0; i < subObject->SlotCount; ++i) { var slotContext = globalContext.CreateContext(subObject, (uint)i, slot, equipment, weaponType); - var imc = (ResourceHandle*)subObject->IMCArray[i]; - var imcNode = slotContext.CreateNodeFromImc(imc); - if (imcNode != null) + var imc = (ResourceHandle*)subObject->IMCArray[i]; + if (slotContext.CreateNodeFromImc(imc) is { } imcNode) { if (globalContext.WithUiData) imcNode.FallbackName = $"Weapon #{weaponIndex}, IMC #{i}"; weaponNodes.Add(imcNode); } - var mdl = subObject->Models[i]; - var mdlNode = slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null); - if (mdlNode != null) + var mdl = subObject->Models[i]; + if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode) { if (globalContext.WithUiData) mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}"; @@ -185,9 +174,11 @@ public class ResourceTree } } - AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, $"Weapon #{weaponIndex}, "); + AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule, + $"Weapon #{weaponIndex}, "); // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) - AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), $"Weapon #{weaponIndex}, "); + AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940), + $"Weapon #{weaponIndex}, "); ++weaponIndex; } @@ -200,28 +191,25 @@ public class ResourceTree var genericContext = globalContext.CreateContext(&human->CharacterBase); var cache = globalContext.Collection._cache; - if (cache != null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)) + if (cache is not null + && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle) + && genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode) { - var pbdNode = genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle); - if (pbdNode != null) + if (globalContext.WithUiData) { - if (globalContext.WithUiData) - { - pbdNode = pbdNode.Clone(); - pbdNode.FallbackName = "Racial Deformer"; - pbdNode.IconFlag = ChangedItemIconFlag.Customization; - } - - Nodes.Add(pbdNode); + pbdNode = pbdNode.Clone(); + pbdNode.FallbackName = "Racial Deformer"; + pbdNode.IconFlag = ChangedItemIconFlag.Customization; } + + Nodes.Add(pbdNode); } var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); - var decalPath = decalId != 0 + var decalPath = decalId is not 0 ? GamePaths.Human.Decal.FaceDecalPath(decalId) : GamePaths.Tex.TransparentPath; - var decalNode = genericContext.CreateNodeFromTex(human->Decal, decalPath); - if (decalNode != null) + if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode) { if (globalContext.WithUiData) { @@ -237,8 +225,7 @@ public class ResourceTree var legacyDecalPath = hasLegacyDecal ? GamePaths.Human.Decal.LegacyDecalPath : GamePaths.Tex.TransparentPath; - var legacyDecalNode = genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath); - if (legacyDecalNode != null) + if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode) { legacyDecalNode.ForceProtected = !hasLegacyDecal; if (globalContext.WithUiData) @@ -252,7 +239,8 @@ public class ResourceTree } } - private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, string prefix = "") + private unsafe void AddSkeleton(List nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics, + string prefix = "") { var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid); if (eidNode != null) @@ -269,8 +257,7 @@ public class ResourceTree { // TODO ClientStructs-ify (aers/FFXIVClientStructs#1312) var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null; - var sklbNode = context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i); - if (sklbNode != null) + if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode) { if (context.Global.WithUiData) sklbNode.FallbackName = $"{prefix}Skeleton #{i}"; @@ -279,10 +266,11 @@ public class ResourceTree } } - private unsafe void AddMaterialAnimationSkeleton(List nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, string prefix = "") + private unsafe void AddMaterialAnimationSkeleton(List nodes, ResolveContext context, SkeletonResourceHandle* sklbHandle, + string prefix = "") { var sklbNode = context.CreateNodeFromMaterialSklb(sklbHandle); - if (sklbNode == null) + if (sklbNode is null) return; if (context.Global.WithUiData) From 9b25193d4e6feb39136b69a522b84b395b35fce2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 05:51:25 +0100 Subject: [PATCH 604/865] ImUtf8 and null-check cleanup. --- .../ModEditWindow.QuickImport.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index a49d2933..00caaabc 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -1,8 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using Lumina.Data; -using OtterGui; -using OtterGui.Raii; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; @@ -43,7 +42,7 @@ public partial class ModEditWindow private void DrawQuickImportTab() { - using var tab = ImRaii.TabItem("Import from Screen"); + using var tab = ImUtf8.TabItem("Import from Screen"u8); if (!tab) { _quickImportActions.Clear(); @@ -73,14 +72,14 @@ public partial class ModEditWindow else { var file = _gameData.GetFile(path); - writable = file == null ? null : new RawGameFileWritable(file); + writable = file is null ? null : new RawGameFileWritable(file); } _quickImportWritables.Add(resourceNode.FullPath, writable); } - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", - resourceNode.FullPath.FullName.Length == 0 || writable == null, true)) + 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 @@ -112,16 +111,17 @@ public partial class ModEditWindow var canQuickImport = quickImport.CanExecute; var quickImportEnabled = canQuickImport && (!resourceNode.Protected || _config.DeleteModModifier.IsActive()); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), buttonSize, + if (ImUtf8.IconButton(FontAwesomeIcon.FileImport, $"Add a copy of this file to {quickImport.OptionName}.{(canQuickImport && !quickImportEnabled ? $"\nHold {_config.DeleteModModifier} while clicking to add." : string.Empty)}", - !quickImportEnabled, true)) + buttonSize, + !quickImportEnabled)) { quickImport.Execute(); _quickImportActions.Remove((resourceNode.GamePath, writable)); } } - private record class RawFileWritable(string Path) : IWritable + private record RawFileWritable(string Path) : IWritable { public bool Valid => true; @@ -130,7 +130,7 @@ public partial class ModEditWindow => File.ReadAllBytes(Path); } - private record class RawGameFileWritable(FileResource FileResource) : IWritable + private record RawGameFileWritable(FileResource FileResource) : IWritable { public bool Valid => true; @@ -188,19 +188,19 @@ public partial class ModEditWindow public static QuickImportAction Prepare(ModEditWindow owner, Utf8GamePath gamePath, IWritable? file) { var editor = owner._editor; - if (editor == null) + if (editor is null) return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); var subMod = editor.Option!; var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName; - if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) + if (gamePath.IsEmpty || file is null || editor.FileEditor.Changes) return new QuickImportAction(editor, optionName, gamePath); if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) return new QuickImportAction(editor, optionName, gamePath); var mod = owner.Mod; - if (mod == null) + if (mod is null) return new QuickImportAction(editor, optionName, gamePath); var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport); @@ -235,7 +235,7 @@ public partial class ModEditWindow { var path = mod.ModPath; var subDirs = 0; - if (subMod == null) + if (subMod is null) return (path, subDirs); var name = subMod.Name; From 8860d1e39afd872eabb140f4dd955896c61d9db7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 06:01:08 +0100 Subject: [PATCH 605/865] Fix an exception in incognito names in weird cutscene cases. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c59b1da6..f42c7fc9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c59b1da61610e656b3e89f9c33113d08f97ae6c7 +Subproject commit f42c7fc9de98e9fc72680dee7805251fd938af26 From c6de7ddebd7af3b52ecb5ebebe86efef07597b1e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 13:08:41 +0100 Subject: [PATCH 606/865] Improve GamePaths and parsing, add support for identifying skeletons and phybs. --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 12 ++++---- .../ResolveContext.PathResolution.cs | 14 ++++----- .../Interop/ResourceTree/ResolveContext.cs | 12 ++++---- Penumbra/Interop/ResourceTree/ResourceTree.cs | 8 ++--- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 8 ++--- .../Manipulations/GlobalEqpManipulation.cs | 10 +++---- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 24 +++++---------- Penumbra/Mods/ItemSwap/CustomizationSwap.cs | 10 +++---- Penumbra/Mods/ItemSwap/EquipmentSwap.cs | 30 +++++++------------ Penumbra/Mods/ItemSwap/ItemSwap.cs | 4 +-- .../Materials/MtrlTab.ShaderPackage.cs | 2 +- .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 4 +-- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- 17 files changed, 65 insertions(+), 83 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index f42c7fc9..bc339208 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit f42c7fc9de98e9fc72680dee7805251fd938af26 +Subproject commit bc339208d1d453582eb146533c572823146a4592 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 0c19bc0a..19d06a52 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -63,7 +63,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM if (info.FileType is not FileType.Model) return []; - var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1); + var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1); return info.ObjectType switch { @@ -79,9 +79,9 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), - ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], - ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], - ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)], + ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)], + ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)], + ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)], _ => [], }; } @@ -105,7 +105,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM if (targetId == EstEntry.Zero) return []; - return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, type.ToName(), targetId.AsId)]; + return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)]; } /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. @@ -137,7 +137,7 @@ public sealed class ModelManager(IFramework framework, MetaFileManager metaFileM var resolvedPath = info.ObjectType switch { - ObjectType.Character => GamePaths.Character.Mtrl.Path( + ObjectType.Character => GamePaths.Mtrl.Customization( info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), _ => absolutePath, }; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs index b1ca24b0..b6d04769 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.PathResolution.cs @@ -43,8 +43,8 @@ internal partial record ResolveContext private Utf8GamePath ResolveEquipmentModelPath() { var path = IsEquipmentSlot(SlotIndex) - ? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()) - : GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); + ? GamePaths.Mdl.Equipment(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()) + : GamePaths.Mdl.Accessory(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot()); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -122,7 +122,7 @@ internal partial record ResolveContext var setIdHigh = Equipment.Set.Id / 100; // All MCH (20??) weapons' materials C are one and the same if (setIdHigh is 20 && mtrlFileName[14] == (byte)'c') - return Utf8GamePath.FromString(GamePaths.Weapon.Mtrl.Path(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; + return Utf8GamePath.FromString(GamePaths.Mtrl.Weapon(2001, 1, 1, "c"), out var path) ? path : Utf8GamePath.Empty; // Some offhands share materials with the corresponding mainhand if (ItemData.AdaptOffhandImc(Equipment.Set, out var mirroredSetId)) @@ -230,7 +230,7 @@ internal partial record ResolveContext if (set == 0) return Utf8GamePath.Empty; - var path = GamePaths.Skeleton.Sklb.Path(raceCode, slot, set); + var path = GamePaths.Sklb.Customization(raceCode, slot, set); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -300,7 +300,7 @@ internal partial record ResolveContext if (set.Id is 0) return Utf8GamePath.Empty; - var path = GamePaths.Skeleton.Skp.Path(raceCode, slot, set); + var path = GamePaths.Skp.Customization(raceCode, slot, set); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -328,7 +328,7 @@ internal partial record ResolveContext if (set.Id is 0) return Utf8GamePath.Empty; - var path = GamePaths.Skeleton.Phyb.Path(raceCode, slot, set); + var path = GamePaths.Phyb.Customization(raceCode, slot, set); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } @@ -354,7 +354,7 @@ internal partial record ResolveContext if (decal is 0) return Utf8GamePath.Empty; - var path = GamePaths.Equipment.Decal.Path(decal); + var path = GamePaths.Tex.EquipDecal(decal); return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty; } } diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 81904819..ea4506c7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -355,13 +355,10 @@ internal unsafe partial record ResolveContext( if (sklbHandle is null) return null; - if (!Utf8GamePath.FromString(GamePaths.Skeleton.Sklb.MaterialAnimationSkeletonPath, out var path)) - return null; - - if (Global.Nodes.TryGetValue((path, (nint)sklbHandle), out var cached)) + if (Global.Nodes.TryGetValue((GamePaths.Sklb.MaterialAnimationSkeletonUtf8, (nint)sklbHandle), out var cached)) return cached; - var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, path); + var node = CreateNode(ResourceType.Sklb, 0, (ResourceHandle*)sklbHandle, GamePaths.Sklb.MaterialAnimationSkeletonUtf8); node.ForceInternal = true; return node; @@ -455,11 +452,12 @@ internal unsafe partial record ResolveContext( internal ResourceNode.UiData GuessUiDataFromPath(Utf8GamePath gamePath) { + const string customization = "Customization: "; foreach (var obj in Global.Identifier.Identify(gamePath.ToString())) { var name = obj.Key; - if (obj.Value is IdentifiedCustomization) - name = name[14..].Trim(); + if (name.StartsWith(customization)) + name = name.AsSpan(14).Trim().ToString(); if (name is not "Unknown") return new ResourceNode.UiData(name, obj.Value.GetIcon().ToFlag()); } diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 5e3f52d4..7be8694a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -207,8 +207,8 @@ public class ResourceTree( var decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F); var decalPath = decalId is not 0 - ? GamePaths.Human.Decal.FaceDecalPath(decalId) - : GamePaths.Tex.TransparentPath; + ? GamePaths.Tex.FaceDecal(decalId) + : GamePaths.Tex.Transparent; if (genericContext.CreateNodeFromTex(human->Decal, decalPath) is { } decalNode) { if (globalContext.WithUiData) @@ -223,8 +223,8 @@ public class ResourceTree( var hasLegacyDecal = (human->Customize[(int)CustomizeIndex.FaceFeatures] & 0x80) != 0; var legacyDecalPath = hasLegacyDecal - ? GamePaths.Human.Decal.LegacyDecalPath - : GamePaths.Tex.TransparentPath; + ? GamePaths.Tex.LegacyDecal + : GamePaths.Tex.Transparent; if (genericContext.CreateNodeFromTex(human->LegacyBodyDecal, legacyDecalPath) is { } legacyDecalNode) { legacyDecalNode.ForceProtected = !hasLegacyDecal; diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 3a804d0c..285f2309 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -16,7 +16,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge => GenderRace.Split().Item1; public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, Slot)); + => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot)); public MetaIndex FileIndex() => CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory()); diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index f758126c..c71f2f4d 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -9,7 +9,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, Slot)); + => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() => MetaIndex.Eqp; diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index cfe9b7d4..007cd02f 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -30,17 +30,17 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende { case EstType.Hair: changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair (Hair) {SetId}", null); + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", null); break; case EstType.Face: changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face (Face) {SetId}", null); + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", null); break; case EstType.Body: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Body)); + identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); break; case EstType.Head: - identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace, EquipSlot.Head)); + identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Head)); break; } } diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index ec59762b..6a1ceaea 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -74,11 +74,11 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier { var path = Type switch { - GlobalEqpType.DoNotHideEarrings => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Ears), - GlobalEqpType.DoNotHideNecklace => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Neck), - GlobalEqpType.DoNotHideBracelets => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists), - GlobalEqpType.DoNotHideRingR => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger), - GlobalEqpType.DoNotHideRingL => GamePaths.Accessory.Mdl.Path(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger), + GlobalEqpType.DoNotHideEarrings => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.Ears), + GlobalEqpType.DoNotHideNecklace => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.Neck), + GlobalEqpType.DoNotHideBracelets => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.Wrists), + GlobalEqpType.DoNotHideRingR => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.RFinger), + GlobalEqpType.DoNotHideRingL => GamePaths.Mdl.Accessory(Condition, GenderRace.MidlanderMale, EquipSlot.LFinger), GlobalEqpType.DoNotHideHrothgarHats => string.Empty, GlobalEqpType.DoNotHideVieraHats => string.Empty, _ => string.Empty, diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 1f41adfb..8cd07bfd 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -9,7 +9,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => identifier.Identify(changedItems, GamePaths.Equipment.Mdl.Path(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); + => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() => MetaIndex.Gmp; diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index cba6c379..6e893043 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -34,14 +34,14 @@ public readonly record struct ImcIdentifier( { var path = ObjectType switch { - ObjectType.Equipment when allVariants => GamePaths.Equipment.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), - ObjectType.Equipment => GamePaths.Equipment.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), - ObjectType.Accessory when allVariants => GamePaths.Accessory.Mdl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot), - ObjectType.Accessory => GamePaths.Accessory.Mtrl.Path(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), - ObjectType.Weapon => GamePaths.Weapon.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), - ObjectType.DemiHuman => GamePaths.DemiHuman.Mtrl.Path(PrimaryId, SecondaryId.Id, EquipSlot, Variant, + ObjectType.Equipment when allVariants => GamePaths.Mdl.Equipment(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Equipment => GamePaths.Mtrl.Equipment(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Accessory when allVariants => GamePaths.Mdl.Accessory(PrimaryId, GenderRace.MidlanderMale, EquipSlot), + ObjectType.Accessory => GamePaths.Mtrl.Accessory(PrimaryId, GenderRace.MidlanderMale, EquipSlot, Variant, "a"), + ObjectType.Weapon => GamePaths.Mtrl.Weapon(PrimaryId, SecondaryId.Id, Variant, "a"), + ObjectType.DemiHuman => GamePaths.Mtrl.DemiHuman(PrimaryId, SecondaryId.Id, EquipSlot, Variant, "a"), - ObjectType.Monster => GamePaths.Monster.Mtrl.Path(PrimaryId, SecondaryId.Id, Variant, "a"), + ObjectType.Monster => GamePaths.Mtrl.Monster(PrimaryId, SecondaryId.Id, Variant, "a"), _ => string.Empty, }; if (path.Length == 0) @@ -51,15 +51,7 @@ public readonly record struct ImcIdentifier( } public string GamePathString() - => ObjectType switch - { - ObjectType.Accessory => GamePaths.Accessory.Imc.Path(PrimaryId), - ObjectType.Equipment => GamePaths.Equipment.Imc.Path(PrimaryId), - ObjectType.DemiHuman => GamePaths.DemiHuman.Imc.Path(PrimaryId, SecondaryId.Id), - ObjectType.Monster => GamePaths.Monster.Imc.Path(PrimaryId, SecondaryId.Id), - ObjectType.Weapon => GamePaths.Weapon.Imc.Path(PrimaryId, SecondaryId.Id), - _ => string.Empty, - }; + => GamePaths.Imc.Path(ObjectType, PrimaryId, SecondaryId); public Utf8GamePath GamePath() => Utf8GamePath.FromString(GamePathString(), out var p) ? p : Utf8GamePath.Empty; diff --git a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs index cd36de93..c5406f66 100644 --- a/Penumbra/Mods/ItemSwap/CustomizationSwap.cs +++ b/Penumbra/Mods/ItemSwap/CustomizationSwap.cs @@ -17,8 +17,8 @@ public static class CustomizationSwap if (idFrom.Id > byte.MaxValue) throw new Exception($"The Customization ID {idFrom} is too large for {slot}."); - var mdlPathFrom = GamePaths.Character.Mdl.Path(race, slot, idFrom, slot.ToCustomizationType()); - var mdlPathTo = GamePaths.Character.Mdl.Path(race, slot, idTo, slot.ToCustomizationType()); + var mdlPathFrom = GamePaths.Mdl.Customization(race, slot, idFrom, slot.ToCustomizationType()); + var mdlPathTo = GamePaths.Mdl.Customization(race, slot, idTo, slot.ToCustomizationType()); var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); var range = slot == BodySlot.Tail @@ -47,8 +47,8 @@ public static class CustomizationSwap ref string fileName, ref bool dataWasChanged) { variant = slot is BodySlot.Face or BodySlot.Ear ? Variant.None.Id : variant; - var mtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); - var mtrlToPath = GamePaths.Character.Mtrl.Path(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); + var mtrlFromPath = GamePaths.Mtrl.Customization(race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant); + var mtrlToPath = GamePaths.Mtrl.Customization(race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant); var newFileName = fileName; newFileName = ItemSwap.ReplaceRace(newFileName, gameRaceTo, race, gameRaceTo != race); @@ -60,7 +60,7 @@ public static class CustomizationSwap var actualMtrlFromPath = mtrlFromPath; if (newFileName != fileName) { - actualMtrlFromPath = GamePaths.Character.Mtrl.Path(race, slot, idFrom, newFileName, out _, out _, variant); + actualMtrlFromPath = GamePaths.Mtrl.Customization(race, slot, idFrom, newFileName, out _, out _, variant); fileName = newFileName; dataWasChanged = true; } diff --git a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs index 8c80c91c..5c67df52 100644 --- a/Penumbra/Mods/ItemSwap/EquipmentSwap.cs +++ b/Penumbra/Mods/ItemSwap/EquipmentSwap.cs @@ -107,7 +107,7 @@ public static class EquipmentSwap foreach (var child in eqp.ChildSwaps.SelectMany(c => c.WithChildren()).OfType>()) { affectedItems.UnionWith(identifier - .Identify(GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, child.SwapFromIdentifier.Slot)) + .Identify(GamePaths.Mdl.Equipment(idFrom, GenderRace.MidlanderMale, child.SwapFromIdentifier.Slot)) .Select(kvp => kvp.Value).OfType().Select(i => i.Item)); } } @@ -223,11 +223,9 @@ public static class EquipmentSwap public static FileSwap CreateMdl(MetaFileManager manager, Func redirections, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, PrimaryId idFrom, PrimaryId idTo, byte mtrlTo) { - var mdlPathFrom = slotFrom.IsAccessory() - ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom) - : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom); - var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo); - var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); + var mdlPathFrom = GamePaths.Mdl.Gear(idFrom, gr, slotFrom); + var mdlPathTo = GamePaths.Mdl.Gear(idTo, gr, slotTo); + var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo); foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan()) { @@ -264,9 +262,7 @@ public static class EquipmentSwap } else { - items = identifier.Identify(slotFrom.IsEquipment() - ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom) - : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)) + items = identifier.Identify(GamePaths.Mdl.Gear(idFrom, GenderRace.MidlanderMale, slotFrom)) .Select(kvp => kvp.Value).OfType().Select(i => i.Item) .ToHashSet(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (Variant)i).ToArray(); @@ -324,7 +320,7 @@ public static class EquipmentSwap if (decalId == 0) return null; - var decalPath = GamePaths.Equipment.Decal.Path(decalId); + var decalPath = GamePaths.Tex.EquipDecal(decalId); return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, decalPath, decalPath); } @@ -337,9 +333,9 @@ public static class EquipmentSwap if (vfxId == 0) return null; - var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom, vfxId); + var vfxPathFrom = GamePaths.Avfx.Path(slotFrom, idFrom, vfxId); vfxPathFrom = ItemSwap.ReplaceType(vfxPathFrom, slotFrom, slotTo, idFrom); - var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo, vfxId); + var vfxPathTo = GamePaths.Avfx.Path(slotTo, idTo, vfxId); var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo); foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan()) @@ -402,14 +398,10 @@ public static class EquipmentSwap if (!fileName.Contains($"{prefix}{idTo.Id:D4}")) return null; - var folderTo = slotTo.IsAccessory() - ? GamePaths.Accessory.Mtrl.FolderPath(idTo, variantTo) - : GamePaths.Equipment.Mtrl.FolderPath(idTo, variantTo); + var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo); var pathTo = $"{folderTo}{fileName}"; - var folderFrom = slotFrom.IsAccessory() - ? GamePaths.Accessory.Mtrl.FolderPath(idFrom, variantTo) - : GamePaths.Equipment.Mtrl.FolderPath(idFrom, variantTo); + var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo); var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom); newFileName = ItemSwap.ReplaceSlot(newFileName, slotTo, slotFrom, slotTo != slotFrom); var pathFrom = $"{folderFrom}{newFileName}"; @@ -457,7 +449,7 @@ public static class EquipmentSwap public static FileSwap CreateShader(MetaFileManager manager, Func redirections, ref string shaderName, ref bool dataWasChanged) { - var path = $"shader/sm5/shpk/{shaderName}"; + var path = GamePaths.Shader(shaderName); return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path); } diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 03abfc45..0049fa12 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -132,14 +132,14 @@ public static class ItemSwap public static FileSwap CreatePhyb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var phybPath = GamePaths.Skeleton.Phyb.Path(race, type.ToName(), estEntry.AsId); + var phybPath = GamePaths.Phyb.Customization(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Phyb, redirections, phybPath, phybPath); } public static FileSwap CreateSklb(MetaFileManager manager, Func redirections, EstType type, GenderRace race, EstEntry estEntry) { - var sklbPath = GamePaths.Skeleton.Sklb.Path(race, type.ToName(), estEntry.AsId); + var sklbPath = GamePaths.Sklb.Customization(race, type.ToName(), estEntry.AsId); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index ae57a122..a13dd96b 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -124,7 +124,7 @@ public partial class MtrlTab private FullPath FindAssociatedShpk(out string defaultPath, out Utf8GamePath defaultGamePath) { - defaultPath = GamePaths.Shader.ShpkPath(Mtrl.ShaderPackage.Name); + defaultPath = GamePaths.Shader(Mtrl.ShaderPackage.Name); if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) return FullPath.Empty; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 66db0932..80b10607 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -55,7 +55,7 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer if (filePath.Length == 0 || !File.Exists(filePath)) throw new FileNotFoundException(); - var gr = GamePaths.ParseRaceCode(filePath); + var gr = Parser.ParseRaceCode(filePath); if (gr is GenderRace.Unknown) throw new Exception($"Could not identify race code from path {filePath}."); var text = File.ReadAllBytes(filePath); @@ -277,7 +277,7 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer if (!ret) return false; - index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint!.Entries.Length - 1)); + index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint.Entries.Length - 1)); identifier = identifier with { EntryIndex = index }; return true; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 1356340c..c9a1d059 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -68,7 +68,7 @@ public partial class ModEditWindow { _dragDropManager.CreateImGuiSource("atchDrag", f => f.Extensions.Contains(".atch"), f => { - var gr = GamePaths.ParseRaceCode(f.Files.FirstOrDefault() ?? string.Empty); + var gr = Parser.ParseRaceCode(f.Files.FirstOrDefault() ?? string.Empty); if (gr is GenderRace.Unknown) return false; From 1ebe4099d6782c60ab3769be2b1801d42bb07480 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Feb 2025 17:51:27 +0100 Subject: [PATCH 607/865] Add ImGuiCacheService. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 0b6085ce..3bf047bf 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 0b6085ce720ffb7c78cf42d4e51861f34db27744 +Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7 From deba8ac9101572d3f7245e6a2fe4c8b33a34e0dc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 00:33:56 +0100 Subject: [PATCH 608/865] Heavily improve changed item display. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/UiApi.cs | 8 +- Penumbra/Api/ModChangedItemAdapter.cs | 2 +- Penumbra/Collections/Cache/CollectionCache.cs | 6 +- .../Collections/ModCollection.Cache.Access.cs | 4 +- Penumbra/Communication/ChangedItemClick.cs | 2 +- Penumbra/Communication/ChangedItemHover.cs | 2 +- Penumbra/Meta/Manipulations/AtchIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 10 +- .../Manipulations/GlobalEqpManipulation.cs | 6 +- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- .../Meta/Manipulations/IMetaIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 4 +- Penumbra/Meta/Manipulations/Rsp.cs | 4 +- Penumbra/Mods/Groups/CombiningModGroup.cs | 2 +- Penumbra/Mods/Groups/IModGroup.cs | 2 +- Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 1 + Penumbra/Mods/Mod.cs | 9 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 3 +- Penumbra/UI/ChangedItemDrawer.cs | 83 +++--- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 253 ++++++++++++++++-- Penumbra/UI/Tabs/ChangedItemsTab.cs | 57 ++-- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- Penumbra/Util/IdentifierExtensions.cs | 6 +- 30 files changed, 360 insertions(+), 126 deletions(-) diff --git a/OtterGui b/OtterGui index 3bf047bf..c347d29d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7 +Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf diff --git a/Penumbra.GameData b/Penumbra.GameData index bc339208..955c4e6b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bc339208d1d453582eb146533c572823146a4592 +Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4 diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index 515874c0..b14f67ae 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) return; - var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); + var (type, id) = data.ToApiObject(); ChangedItemClicked.Invoke(button, type, id); } - private void OnChangedItemHover(IIdentifiedObjectData? data) + private void OnChangedItemHover(IIdentifiedObjectData data) { if (ChangedItemTooltip == null) return; - var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); + var (type, id) = data.ToApiObject(); ChangedItemTooltip.Invoke(type, id); } } diff --git a/Penumbra/Api/ModChangedItemAdapter.cs b/Penumbra/Api/ModChangedItemAdapter.cs index 8842f20a..8d2d473c 100644 --- a/Penumbra/Api/ModChangedItemAdapter.cs +++ b/Penumbra/Api/ModChangedItemAdapter.cs @@ -65,7 +65,7 @@ public sealed class ModChangedItemAdapter(WeakReference storage) : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed."); } - private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary + private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary { public IEnumerator> GetEnumerator() => data.Select(d => new KeyValuePair(d.Key, d.Value?.ToInternalObject())).GetEnumerator(); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index a80928d0..42c8b27d 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -23,7 +23,7 @@ public sealed class CollectionCache : IDisposable private readonly CollectionCacheManager _manager; private readonly ModCollection _collection; public readonly CollectionModData ModData = new(); - private readonly SortedList, IIdentifiedObjectData?)> _changedItems = []; + private readonly SortedList, IIdentifiedObjectData)> _changedItems = []; public readonly ConcurrentDictionary ResolvedFiles = new(); public readonly CustomResourceCache CustomResources; public readonly MetaCache Meta; @@ -43,7 +43,7 @@ public sealed class CollectionCache : IDisposable private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems + public IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems { get { @@ -441,7 +441,7 @@ public sealed class CollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = _manager.MetaFileManager.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 0b38dde8..716b153e 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -46,8 +46,8 @@ public partial class ModCollection internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); - internal IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData?)>(); + internal IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>(); internal IEnumerable> AllConflicts => _cache?.AllConflicts ?? Array.Empty>(); diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 1aac4454..2d27f36a 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -12,7 +12,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 4e72b558..92d770f7 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -10,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs index bce37620..c248c48b 100644 --- a/Penumbra/Meta/Manipulations/AtchIdentifier.cs +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -31,7 +31,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac public override string ToString() => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { // Nothing specific } diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 285f2309..c8423b92 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index c71f2f4d..154aca40 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 007cd02f..8a450eee 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -24,17 +24,17 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { switch (Slot) { case EstType.Hair: - changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", null); + changedItems.UpdateCountOrSet( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", () => new IdentifiedName()); break; case EstType.Face: - changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", null); + changedItems.UpdateCountOrSet( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", () => new IdentifiedName()); break; case EstType.Body: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 6a1ceaea..1365d9d3 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = Type switch { @@ -86,9 +86,9 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier if (path.Length > 0) identifier.Identify(changedItems, path); else if (Type is GlobalEqpType.DoNotHideVieraHats) - changedItems["All Hats for Viera"] = null; + changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName()); else if (Type is GlobalEqpType.DoNotHideHrothgarHats) - changedItems["All Hats for Hrothgar"] = null; + changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName()); } public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 8cd07bfd..5bc81f26 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 999fd906..c897bb2a 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -19,7 +19,7 @@ public enum MetaManipulationType : byte public interface IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); public MetaIndex FileIndex(); diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 6e893043..fa726708 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 9dc4fe90..5f91a37c 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -8,8 +8,8 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => changedItems.UpdateCountOrSet($"{SubRace.ToName()} {Attribute.ToFullString()}", () => new IdentifiedName()); public MetaIndex FileIndex() => MetaIndex.HumanCmp; diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 80f3c4c0..90a962b7 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -136,7 +136,7 @@ public sealed class CombiningModGroup : IModGroup public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) => Data[setting.AsIndex].AddDataTo(redirections, manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 96422caf..cc961b0f 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -53,7 +53,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 2a1854ed..5ec32274 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -131,7 +131,7 @@ public class ImcModGroup(Mod mod) : IModGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 0c9aa805..82555314 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -122,7 +122,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index ab0c2d4f..c250182a 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -107,7 +107,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 4bf22272..130c8fcb 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -139,6 +139,7 @@ public class ModCacheManager : IDisposable, IService mod.ChangedItems.RemoveMachinistOffhands(); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); + ++mod.LastChangedItemsUpdate; } private static void UpdateCounts(Mod mod) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 488e3dc1..9829d5a0 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -101,13 +101,14 @@ public sealed class Mod : IMod } // Cache - public readonly SortedList ChangedItems = new(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; - public int TotalFileCount { get; internal set; } - public int TotalSwapCount { get; internal set; } - public int TotalManipulations { get; internal set; } + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public ushort LastChangedItemsUpdate { get; internal set; } public bool HasOptions { get; internal set; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3482f620..eb9aa93d 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -25,7 +25,6 @@ public class ResourceTreeViewer( private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly CommunicatorService _communicator = communicator; private readonly HashSet _unfolded = []; private readonly Dictionary _filterCache = []; @@ -278,7 +277,7 @@ public class ResourceTreeViewer( if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) - _communicator.SelectTab.Invoke(TabType.Mods, mod); + communicator.SelectTab.Invoke(TabType.Mods, mod); ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index af9782d5..a9070360 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -9,6 +9,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Services; @@ -86,18 +87,20 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) + public bool FilterChangedItem(string name, IIdentifiedObjectData data, LowerString filter) => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) && (filter.IsEmpty || !data.IsFilteredOut(name, filter)); /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(IIdentifiedObjectData? data) - => DrawCategoryIcon(data.GetIcon().ToFlag()); + public void DrawCategoryIcon(IIdentifiedObjectData data, float height) + => DrawCategoryIcon(data.GetIcon().ToFlag(), height); public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) + => DrawCategoryIcon(iconFlagType, ImGui.GetFrameHeight()); + + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height) { - var height = ImGui.GetFrameHeight(); if (!_icons.TryGetValue(iconFlagType, out var icon)) { ImGui.Dummy(new Vector2(height)); @@ -114,50 +117,50 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - /// - /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item ID in grey if requested. - /// - public void DrawChangedItem(string name, IIdentifiedObjectData? data) + public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked) { - name = data?.ToName(name) ?? name; - using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) - .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) - { - var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) - ? MouseButton.Left - : MouseButton.None; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; - if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, data); - } + var ret = leftClicked ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + if (ret != MouseButton.None) + _communicator.ChangedItemClick.Invoke(ret, data); + if (!ImGui.IsItemHovered()) + return; - if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) - { - // We can not be sure that any subscriber actually prints something in any case. - // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using (ImRaii.Group()) - { - _communicator.ChangedItemHover.Invoke(data); - } - - if (ImGui.GetItemRectSize() == Vector2.Zero) - ImGui.TextUnformatted("No actions available."); - } + using var tt = ImUtf8.Tooltip(); + if (data.Count == 1) + ImUtf8.Text("This item is changed through a single effective change.\n"); + else + ImUtf8.Text($"This item is changed through {data.Count} distinct effective changes.\n"); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); + ImGui.Separator(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); + _communicator.ChangedItemHover.Invoke(data); } /// Draw the model information, right-justified. - public static void DrawModelData(IIdentifiedObjectData? data) + public static void DrawModelData(IIdentifiedObjectData data, float height) { - var additionalData = data?.AdditionalData ?? string.Empty; + var additionalData = data.AdditionalData; if (additionalData.Length == 0) return; - ImGui.SameLine(ImGui.GetContentRegionAvail().X); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); + ImUtf8.TextRightAligned(additionalData, ImGui.GetStyle().ItemInnerSpacing.X); + } + + /// Draw the model information, right-justified. + public static void DrawModelData(ReadOnlySpan text, float height) + { + if (text.Length == 0) + return; + + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); + ImUtf8.TextRightAligned(text, ImGui.GetStyle().ItemInnerSpacing.X); } /// Draw a header line with the different icon types to filter them. @@ -276,7 +279,7 @@ public class ChangedItemDrawer : IDisposable, IUiService return true; } - private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) + private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index a7bdadd3..ac4fd167 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,47 +1,268 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui; using OtterGui.Classes; -using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods; +using Penumbra.String; namespace Penumbra.UI.ModsTab; -public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) : ITab, IUiService +public class ModPanelChangedItemsTab( + ModFileSystemSelector selector, + ChangedItemDrawer drawer, + ImGuiCacheService cacheService, + EphemeralConfig config) + : ITab, IUiService { + private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); + + private class ChangedItemsCache + { + private Mod? _lastSelected; + private ushort _lastUpdate; + private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags; + private bool _reset; + public readonly List Data = []; + public bool AnyExpandable { get; private set; } + + public record struct Container + { + public IIdentifiedObjectData Data; + public ByteString Text; + public ByteString ModelData; + public uint Id; + public int Children; + public ChangedItemIconFlag Icon; + public bool Expandable; + public bool Expanded; + public bool Child; + + public static Container Single(string text, IIdentifiedObjectData data) + => new() + { + Child = false, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = false, + Expanded = false, + Data = data, + Id = 0, + Children = 0, + }; + + public static Container Parent(string text, IIdentifiedObjectData data, uint id, int children, bool expanded) + => new() + { + Child = false, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = true, + Expanded = expanded, + Data = data, + Id = id, + Children = children, + }; + + public static Container Indent(string text, IIdentifiedObjectData data) + => new() + { + Child = true, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = false, + Expanded = false, + Data = data, + Id = 0, + Children = 0, + }; + } + + public void Reset() + => _reset = true; + + public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter) + { + if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset) + return; + + _reset = false; + Data.Clear(); + AnyExpandable = false; + _lastSelected = mod; + _filter = filter; + if (_lastSelected == null) + return; + + _lastUpdate = _lastSelected.LastChangedItemsUpdate; + var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (i is not IdentifiedItem item) + continue; + + if (!drawer.FilterChangedItem(s, item, LowerString.Empty)) + continue; + + if (tmp.TryGetValue((item.Item.PrimaryId, item.Item.Type), out var p)) + p.Add(item); + else + tmp[(item.Item.PrimaryId, item.Item.Type)] = [item]; + } + + foreach (var list in tmp.Values) + { + list.Sort((i1, i2) => + { + // reversed + var count = i2.Count.CompareTo(i1.Count); + if (count != 0) + return count; + + return string.Compare(i1.Item.Name, i2.Item.Name, StringComparison.Ordinal); + }); + } + + var sortedTmp = tmp.Values.OrderBy(s => s[0].Item.Name).ToArray(); + + var sortedTmpIdx = 0; + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (i is IdentifiedItem) + continue; + + if (!drawer.FilterChangedItem(s, i, LowerString.Empty)) + continue; + + while (sortedTmpIdx < sortedTmp.Length + && string.Compare(sortedTmp[sortedTmpIdx][0].Item.Name, s, StringComparison.Ordinal) <= 0) + AddList(sortedTmp[sortedTmpIdx++]); + + Data.Add(Container.Single(s, i)); + } + + for (; sortedTmpIdx < sortedTmp.Length; ++sortedTmpIdx) + AddList(sortedTmp[sortedTmpIdx]); + return; + + void AddList(List list) + { + var mainItem = list[0]; + if (list.Count == 1) + { + Data.Add(Container.Single(mainItem.Item.Name, mainItem)); + } + else + { + var id = ImUtf8.GetId($"{mainItem.Item.PrimaryId}{(int)mainItem.Item.Type}"); + var expanded = ImGui.GetStateStorage().GetBool(id, false); + Data.Add(Container.Parent(mainItem.Item.Name, mainItem, id, list.Count - 1, expanded)); + AnyExpandable = true; + if (!expanded) + return; + + foreach (var item in list.Skip(1)) + Data.Add(Container.Indent(item.Item.Name, item)); + } + } + } + } + public ReadOnlySpan Label => "Changed Items"u8; public bool IsVisible => selector.Selected!.ChangedItems.Count > 0; + private ImGuiStoragePtr _stateStorage; + + private Vector2 _buttonSize; + public void DrawContent() { + if (cacheService.Cache(_cacheId, () => (new ChangedItemsCache(), "ModPanelChangedItemsCache")) is not { } cache) + return; + drawer.DrawTypeFilter(); + + _stateStorage = ImGui.GetStateStorage(); + cache.Update(selector.Selected, drawer, config.ChangedItemFilter); ImGui.Separator(); - using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, + _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) + .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); if (!table) return; - var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); - var height = ImGui.GetFrameHeightWithSpacing(); - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(height); - var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem); - ImGuiClip.DrawEndDummy(remainder, height); + if (cache.AnyExpandable) + { + ImUtf8.TableSetupColumn("##exp"u8, ImGuiTableColumnFlags.WidthFixed, _buttonSize.Y); + ImUtf8.TableSetupColumn("##text"u8, ImGuiTableColumnFlags.WidthStretch); + ImGuiClip.ClippedDraw(cache.Data, DrawContainerExpandable, _buttonSize.Y); + } + else + { + ImGuiClip.ClippedDraw(cache.Data, DrawContainer, ImGui.GetFrameHeightWithSpacing()); + } } - private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) - => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + private void DrawContainerExpandable(ChangedItemsCache.Container obj, int idx) + { + using var id = ImUtf8.PushId(idx); + ImGui.TableNextColumn(); + if (obj.Expandable) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, 0); + if (ImUtf8.IconButton(obj.Expanded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight, + obj.Expanded ? "Hide the other items using the same model." : + obj.Children > 1 ? $"Show {obj.Children} other items using the same model." : + "Show one other item using the same model.", + _buttonSize)) + { + _stateStorage.SetBool(obj.Id, !obj.Expanded); + if (cacheService.TryGetCache(_cacheId, out var cache)) + cache.Reset(); + } + } + else + { + ImGui.Dummy(_buttonSize); + } - private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) + DrawBaseContainer(obj, idx); + } + + private void DrawContainer(ChangedItemsCache.Container obj, int idx) + { + using var id = ImUtf8.PushId(idx); + DrawBaseContainer(obj, idx); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(kvp.Data); - ImGui.SameLine(); - drawer.DrawChangedItem(kvp.Name, kvp.Data); - ChangedItemDrawer.DrawModelData(kvp.Data); + using var indent = ImRaii.PushIndent(1, obj.Child); + drawer.DrawCategoryIcon(obj.Icon, _buttonSize.Y); + ImGui.SameLine(0, 0); + var clicked = ImUtf8.Selectable(obj.Text.Span, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(obj.Data, clicked); + ChangedItemDrawer.DrawModelData(obj.ModelData.Span, _buttonSize.Y); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 5bac7d35..6cee22d6 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -26,30 +27,36 @@ public class ChangedItemsTab( private LowerString _changedItemFilter = LowerString.Empty; private LowerString _changedItemModFilter = LowerString.Empty; + private Vector2 _buttonSize; public void DrawContent() { collectionHeader.Draw(true); drawer.DrawTypeFilter(); var varWidth = DrawFilters(); - using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); + using var child = ImUtf8.Child("##changedItemsChild"u8, -Vector2.One); if (!child) return; - var height = ImGui.GetFrameHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips(height); - using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); + _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) + .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + var skips = ImGuiClip.GetNecessarySkips(_buttonSize.Y); + using var list = ImUtf8.Table("##changedItems"u8, 3, ImGuiTableFlags.RowBg, -Vector2.One); if (!list) return; const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn("items", flags, 450 * UiHelpers.Scale); - ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); - ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("items"u8, flags, 450 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("mods"u8, flags, varWidth - 140 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("id"u8, flags, 140 * UiHelpers.Scale); var items = collectionManager.Active.Current.ChangedItems; var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); - ImGuiClip.DrawEndDummy(rest, height); + ImGuiClip.DrawEndDummy(rest, _buttonSize.Y); } /// Draw a pair of filters and return the variable width of the flexible column. @@ -67,22 +74,25 @@ public class ChangedItemsTab( } /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData?)> item) + private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData)> item) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData?)> item) + private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData)> item) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Value.Item2); - ImGui.SameLine(); - drawer.DrawChangedItem(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Value.Item2, _buttonSize.Y); + ImGui.SameLine(0, 0); + var name = item.Value.Item2.ToName(item.Key); + var clicked = ImUtf8.Selectable(name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(item.Value.Item2, clicked); + ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - ChangedItemDrawer.DrawModelData(item.Value.Item2); + ChangedItemDrawer.DrawModelData(item.Value.Item2, _buttonSize.Y); } private void DrawModColumn(SingleArray mods) @@ -90,19 +100,18 @@ public class ChangedItemsTab( if (mods.Count <= 0) return; - var first = mods[0]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); - if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + var first = mods[0]; + if (ImUtf8.Selectable(first.Name.Text, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }) && ImGui.GetIO().KeyCtrl && first is Mod mod) communicator.SelectTab.Invoke(TabType.Mods, mod); - if (ImGui.IsItemHovered()) - { - using var _ = ImRaii.Tooltip(); - ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); - if (mods.Count > 1) - ImGui.TextUnformatted("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); - } + if (!ImGui.IsItemHovered()) + return; + + using var _ = ImRaii.Tooltip(); + ImUtf8.Text("Hold Control and click to jump to mod.\n"u8); + if (mods.Count > 1) + ImUtf8.Text("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 8f76a54a..42502290 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -748,7 +748,7 @@ public class DebugTab : Window, ITab, IUiService } private string _changedItemPath = string.Empty; - private readonly Dictionary _changedItems = []; + private readonly Dictionary _changedItems = []; private void DrawChangedItemTest() { diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 5bd3f77c..f744e940 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -10,7 +10,7 @@ namespace Penumbra.Util; public static class IdentifierExtensions { public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, - IDictionary changedItems) + IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); @@ -19,7 +19,7 @@ public static class IdentifierExtensions manip.AddChangedItems(identifier, changedItems); } - public static void RemoveMachinistOffhands(this SortedList changedItems) + public static void RemoveMachinistOffhands(this SortedList changedItems) { for (var i = 0; i < changedItems.Count; i++) { @@ -31,7 +31,7 @@ public static class IdentifierExtensions } } - public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData?)> changedItems) + public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData)> changedItems) { for (var i = 0; i < changedItems.Count; i++) { From 26985e01a20c83c46c00a1107bda7fe9292132b3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Feb 2025 23:37:03 +0000 Subject: [PATCH 609/865] [CI] Updating repo.json for testing_1.3.4.5 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index afb2c32d..0b8d89cf 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.4", + "TestingAssemblyVersion": "1.3.4.5", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.5/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 13adbd54667e4eff64812d7e281c7d26dfb9689c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 16:55:50 +0100 Subject: [PATCH 610/865] Allow configuration of the changed item display. --- Penumbra/ChangedItemMode.cs | 57 ++++++++++++++ Penumbra/Configuration.cs | 78 ++++++++++++++----- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 26 +++++-- Penumbra/UI/Tabs/SettingsTab.cs | 9 +++ 4 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 Penumbra/ChangedItemMode.cs diff --git a/Penumbra/ChangedItemMode.cs b/Penumbra/ChangedItemMode.cs new file mode 100644 index 00000000..dccffded --- /dev/null +++ b/Penumbra/ChangedItemMode.cs @@ -0,0 +1,57 @@ +using ImGuiNET; +using OtterGui.Text; + +namespace Penumbra; + +public enum ChangedItemMode +{ + GroupedCollapsed, + GroupedExpanded, + Alphabetical, +} + +public static class ChangedItemModeExtensions +{ + public static ReadOnlySpan ToName(this ChangedItemMode mode) + => mode switch + { + ChangedItemMode.GroupedCollapsed => "Grouped (Collapsed)"u8, + ChangedItemMode.GroupedExpanded => "Grouped (Expanded)"u8, + ChangedItemMode.Alphabetical => "Alphabetical"u8, + _ => "Error"u8, + }; + + public static ReadOnlySpan ToTooltip(this ChangedItemMode mode) + => mode switch + { + ChangedItemMode.GroupedCollapsed => + "Display items as groups by their model and slot. Collapse those groups to a single item by default. Prefers items with more changes affecting them or configured items as the main item."u8, + ChangedItemMode.GroupedExpanded => + "Display items as groups by their model and slot. Expand those groups showing all items by default. Prefers items with more changes affecting them or configured items as the main item."u8, + ChangedItemMode.Alphabetical => "Display all changed items in a single list sorted alphabetically."u8, + _ => ""u8, + }; + + public static bool DrawCombo(ReadOnlySpan label, ChangedItemMode value, float width, Action setter) + { + ImGui.SetNextItemWidth(width); + using var combo = ImUtf8.Combo(label, value.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var newValue in Enum.GetValues()) + { + var selected = ImUtf8.Selectable(newValue.ToName(), newValue == value); + if (selected) + { + ret = true; + setter(newValue); + } + + ImUtf8.HoverTooltip(newValue.ToTooltip()); + } + + return ret; + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index ce86dd4a..939eb122 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -39,11 +39,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool EnableMods { get => _enableMods; - set - { - _enableMods = value; - ModsEnabled?.Invoke(value); - } + set => SetField(ref _enableMods, value, ModsEnabled); } public string ModDirectory { get; set; } = string.Empty; @@ -58,21 +54,22 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool AutoSelectCollection { get; set; } = false; - public bool ShowModsInLobby { get; set; } = true; - public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; - public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; - public bool HideChangedItemFilters { get; set; } = false; - public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; - public bool HideMachinistOffhandFromChangedItems { get; set; } = true; - public bool DefaultTemporaryMode { get; set; } = false; - public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; - public int OptionGroupCollapsibleMin { get; set; } = 5; + public bool ShowModsInLobby { get; set; } = true; + public bool UseCharacterCollectionInMainWindow { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseOwnerNameForCharacterCollection { get; set; } = true; + public bool UseNoModsInInspect { get; set; } = false; + public bool HideChangedItemFilters { get; set; } = false; + public bool ReplaceNonAsciiOnImport { get; set; } = false; + public bool HidePrioritiesInSelector { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public bool HideMachinistOffhandFromChangedItems { get; set; } = true; + public bool DefaultTemporaryMode { get; set; } = false; + 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); @@ -217,4 +214,45 @@ public class Configuration : IPluginConfiguration, ISavable, IService var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetField(ref T field, T value, Action? @event, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(value)) + return false; + + var oldValue = field; + field = value; + try + { + @event?.Invoke(oldValue, field); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} from {oldValue} to {field}:\n{ex}"); + throw; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetField(ref T field, T value, Action? @event, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(value)) + return false; + + field = value; + try + { + @event?.Invoke(field); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Error in subscribers updating configuration field {propertyName} to {field}:\n{ex}"); + throw; + } + + return true; + } } diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index ac4fd167..f97e4d51 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -18,7 +18,7 @@ public class ModPanelChangedItemsTab( ModFileSystemSelector selector, ChangedItemDrawer drawer, ImGuiCacheService cacheService, - EphemeralConfig config) + Configuration config) : ITab, IUiService { private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); @@ -28,6 +28,7 @@ public class ModPanelChangedItemsTab( private Mod? _lastSelected; private ushort _lastUpdate; private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags; + private ChangedItemMode _lastMode; private bool _reset; public readonly List Data = []; public bool AnyExpandable { get; private set; } @@ -90,9 +91,9 @@ public class ModPanelChangedItemsTab( public void Reset() => _reset = true; - public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter) + public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter, ChangedItemMode mode) { - if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset) + if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset && _lastMode == mode) return; _reset = false; @@ -100,12 +101,25 @@ public class ModPanelChangedItemsTab( AnyExpandable = false; _lastSelected = mod; _filter = filter; + _lastMode = mode; if (_lastSelected == null) return; _lastUpdate = _lastSelected.LastChangedItemsUpdate; - var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + if (mode is ChangedItemMode.Alphabetical) + { + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (drawer.FilterChangedItem(s, i, LowerString.Empty)) + Data.Add(Container.Single(s, i)); + } + + return; + } + + var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + var defaultExpansion = _lastMode is ChangedItemMode.GroupedExpanded; foreach (var (s, i) in _lastSelected.ChangedItems) { if (i is not IdentifiedItem item) @@ -165,7 +179,7 @@ public class ModPanelChangedItemsTab( else { var id = ImUtf8.GetId($"{mainItem.Item.PrimaryId}{(int)mainItem.Item.Type}"); - var expanded = ImGui.GetStateStorage().GetBool(id, false); + var expanded = ImGui.GetStateStorage().GetBool(id, defaultExpansion); Data.Add(Container.Parent(mainItem.Item.Name, mainItem, id, list.Count - 1, expanded)); AnyExpandable = true; if (!expanded) @@ -196,7 +210,7 @@ public class ModPanelChangedItemsTab( drawer.DrawTypeFilter(); _stateStorage = ImGui.GetStateStorage(); - cache.Update(selector.Selected, drawer, config.ChangedItemFilter); + cache.Update(selector.Selected, drawer, config.Ephemeral.ChangedItemFilter, config.ChangedItemDisplay); ImGui.Separator(); _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 4ff0cd42..ba226aa8 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -445,6 +445,15 @@ public class SettingsTab : ITab, IUiService _config.Ephemeral.Save(); } }); + + ChangedItemModeExtensions.DrawCombo("##ChangedItemMode"u8, _config.ChangedItemDisplay, UiHelpers.InputTextWidth.X, v => + { + _config.ChangedItemDisplay = v; + _config.Save(); + }); + ImUtf8.LabeledHelpMarker("Mod Changed Item Display"u8, + "Configure how to display the changed items of a single mod in the mods info panel."u8); + Checkbox("Omit Machinist Offhands in Changed Items", "Omits all Aetherotransformers (machinist offhands) in the changed items tabs because any change on them changes all of them at the moment.\n\n" + "Changing this triggers a rediscovery of your mods so all changed items can be updated.", From 509f11561aee52acd23a56f7c35d6f4494572384 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 22:21:36 +0100 Subject: [PATCH 611/865] Add preferred changed items to mods. --- Penumbra/Mods/Manager/ModDataEditor.cs | 158 ++++++++++++++++-- Penumbra/Mods/Mod.cs | 28 ++-- Penumbra/Mods/ModLocalData.cs | 27 ++- Penumbra/Mods/ModMeta.cs | 12 +- .../UI/ModsTab/ModPanelChangedItemsTab.cs | 71 +++++++- schemas/local_mod_data-v3.json | 9 + schemas/mod_meta-v3.json | 9 + 7 files changed, 273 insertions(+), 41 deletions(-) diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 7c48205a..1349b525 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -1,7 +1,8 @@ using Dalamud.Utility; -using Newtonsoft.Json.Linq; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -9,23 +10,25 @@ namespace Penumbra.Mods.Manager; [Flags] public enum ModDataChangeType : ushort { - None = 0x0000, - Name = 0x0001, - Author = 0x0002, - Description = 0x0004, - Version = 0x0008, - Website = 0x0010, - Deletion = 0x0020, - Migration = 0x0040, - ModTags = 0x0080, - ImportDate = 0x0100, - Favorite = 0x0200, - LocalTags = 0x0400, - Note = 0x0800, - Image = 0x1000, + None = 0x0000, + Name = 0x0001, + Author = 0x0002, + Description = 0x0004, + Version = 0x0008, + Website = 0x0010, + Deletion = 0x0020, + Migration = 0x0040, + ModTags = 0x0080, + ImportDate = 0x0100, + Favorite = 0x0200, + LocalTags = 0x0400, + Note = 0x0800, + Image = 0x1000, + DefaultChangedItems = 0x2000, + PreferredChangedItems = 0x4000, } -public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService) : IService +public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService { public SaveService SaveService => saveService; @@ -35,7 +38,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic string? website) { var mod = new Mod(directory); - mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name!); + mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name); mod.Author = author != null ? new LowerString(author) : mod.Author; mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; @@ -175,4 +178,125 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic Penumbra.Log.Error($"Could not move local data file {oldFile} to {newFile}:\n{e}"); } } + + public void AddPreferredItem(Mod mod, CustomItemId id, bool toDefault, bool cleanExisting) + { + if (CleanExisting(mod.PreferredChangedItems)) + { + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + if (toDefault && CleanExisting(mod.DefaultPreferredItems)) + { + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + + bool CleanExisting(HashSet items) + { + if (!items.Add(id)) + return false; + + if (!cleanExisting) + return true; + + var it1Exists = itemData.Primary.TryGetValue(id, out var it1); + var it2Exists = itemData.Secondary.TryGetValue(id, out var it2); + var it3Exists = itemData.Tertiary.TryGetValue(id, out var it3); + + foreach (var item in items.ToArray()) + { + if (item == id) + continue; + + if (it1Exists + && itemData.Primary.TryGetValue(item, out var oldItem1) + && oldItem1.PrimaryId == it1.PrimaryId + && oldItem1.Type == it1.Type) + items.Remove(item); + + else if (it2Exists + && itemData.Primary.TryGetValue(item, out var oldItem2) + && oldItem2.PrimaryId == it2.PrimaryId + && oldItem2.Type == it2.Type) + items.Remove(item); + + else if (it3Exists + && itemData.Primary.TryGetValue(item, out var oldItem3) + && oldItem3.PrimaryId == it3.PrimaryId + && oldItem3.Type == it3.Type) + items.Remove(item); + } + + return true; + } + } + + public void RemovePreferredItem(Mod mod, CustomItemId id, bool fromDefault) + { + if (!fromDefault && mod.PreferredChangedItems.Remove(id)) + { + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + if (fromDefault && mod.DefaultPreferredItems.Remove(id)) + { + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + } + + public void ClearInvalidPreferredItems(Mod mod) + { + var currentChangedItems = mod.ChangedItems.Values.OfType().Select(i => i.Item.Id).Distinct().ToHashSet(); + var newSet = new HashSet(mod.PreferredChangedItems.Count); + + if (CheckItems(mod.PreferredChangedItems)) + { + mod.PreferredChangedItems = newSet; + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } + + newSet = new HashSet(mod.DefaultPreferredItems.Count); + if (CheckItems(mod.DefaultPreferredItems)) + { + mod.DefaultPreferredItems = newSet; + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.DefaultChangedItems, mod, null); + } + + return; + + bool CheckItems(HashSet set) + { + var changes = false; + foreach (var item in set) + { + if (currentChangedItems.Contains(item)) + newSet.Add(item); + else + changes = true; + } + + return changes; + } + } + + public void ResetPreferredItems(Mod mod) + { + if (mod.PreferredChangedItems.SetEquals(mod.DefaultPreferredItems)) + return; + + mod.PreferredChangedItems.Clear(); + mod.PreferredChangedItems.UnionWith(mod.DefaultPreferredItems); + ++mod.LastChangedItemsUpdate; + saveService.QueueSave(new ModLocalData(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.PreferredChangedItems, mod, null); + } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 9829d5a0..efd92631 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,6 +1,7 @@ using OtterGui; using OtterGui.Classes; using Penumbra.GameData.Data; +using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; @@ -47,21 +48,22 @@ public sealed class Mod : IMod => Name.Text; // Meta Data - public LowerString Name { get; internal set; } = "New Mod"; - public LowerString Author { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string Version { get; internal set; } = string.Empty; - public string Website { get; internal set; } = string.Empty; - public string Image { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = []; + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public string Image { get; internal set; } = string.Empty; + public IReadOnlyList ModTags { get; internal set; } = []; + public HashSet DefaultPreferredItems { get; internal set; } = []; // Local Data - public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - public IReadOnlyList LocalTags { get; internal set; } = []; - public string Note { get; internal set; } = string.Empty; - public bool Favorite { get; internal set; } = false; - + public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + public IReadOnlyList LocalTags { get; internal set; } = []; + public string Note { get; internal set; } = string.Empty; + public HashSet PreferredChangedItems { get; internal set; } = []; + public bool Favorite { get; internal set; } = false; // Options public readonly DefaultSubMod Default; @@ -110,5 +112,5 @@ public sealed class Mod : IMod public int TotalSwapCount { get; internal set; } public int TotalManipulations { get; internal set; } public ushort LastChangedItemsUpdate { get; internal set; } - public bool HasOptions { get; internal set; } + public bool HasOptions { get; internal set; } } diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs index d3534391..cc20fad6 100644 --- a/Penumbra/Mods/ModLocalData.cs +++ b/Penumbra/Mods/ModLocalData.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -21,6 +22,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable { nameof(Mod.LocalTags), JToken.FromObject(mod.LocalTags) }, { nameof(Mod.Note), JToken.FromObject(mod.Note) }, { nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) }, + { nameof(Mod.PreferredChangedItems), JToken.FromObject(mod.PreferredChangedItems) }, }; using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; @@ -36,6 +38,8 @@ public readonly struct ModLocalData(Mod mod) : ISavable var favorite = false; var note = string.Empty; + HashSet preferredChangedItems = []; + var save = true; if (File.Exists(dataFile)) try @@ -43,16 +47,21 @@ public readonly struct ModLocalData(Mod mod) : ISavable var text = File.ReadAllText(dataFile); var json = JObject.Parse(text); - importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; - favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; - note = json[nameof(Mod.Note)]?.Value() ?? note; - localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; - save = false; + importDate = json[nameof(Mod.ImportDate)]?.Value() ?? importDate; + favorite = json[nameof(Mod.Favorite)]?.Value() ?? favorite; + note = json[nameof(Mod.Note)]?.Value() ?? note; + localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values().OfType() ?? localTags; + preferredChangedItems = (json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values().Select(i => (CustomItemId) i).ToHashSet() ?? mod.DefaultPreferredItems; + save = false; } catch (Exception e) { Penumbra.Log.Error($"Could not load local mod data:\n{e}"); } + else + { + preferredChangedItems = mod.DefaultPreferredItems; + } if (importDate == 0) importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -64,7 +73,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable changes |= ModDataChangeType.ImportDate; } - changes |= ModLocalData.UpdateTags(mod, null, localTags); + changes |= UpdateTags(mod, null, localTags); if (mod.Favorite != favorite) { @@ -78,6 +87,12 @@ public readonly struct ModLocalData(Mod mod) : ISavable changes |= ModDataChangeType.Note; } + if (!preferredChangedItems.SetEquals(mod.PreferredChangedItems)) + { + mod.PreferredChangedItems = preferredChangedItems; + changes |= ModDataChangeType.PreferredChangedItems; + } + if (save) editor.SaveService.QueueSave(new ModLocalData(mod)); diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 0cebcf81..1b104af4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -25,6 +26,7 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.Version), JToken.FromObject(mod.Version) }, { nameof(Mod.Website), JToken.FromObject(mod.Website) }, { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, + { nameof(Mod.DefaultPreferredItems), JToken.FromObject(mod.DefaultPreferredItems) }, }; using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; @@ -48,7 +50,7 @@ public readonly struct ModMeta(Mod mod) : ISavable var newFileVersion = json[nameof(FileVersion)]?.Value() ?? 0; // Empty name gets checked after loading and is not allowed. - var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; + var newName = json[nameof(Mod.Name)]?.Value() ?? string.Empty; var newAuthor = json[nameof(Mod.Author)]?.Value() ?? string.Empty; var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; @@ -56,6 +58,8 @@ public readonly struct ModMeta(Mod mod) : ISavable var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); + var defaultItems = (json[nameof(Mod.DefaultPreferredItems)] as JArray)?.Values().Select(i => (CustomItemId)i).ToHashSet() + ?? []; ModDataChangeType changes = 0; if (mod.Name != newName) @@ -94,6 +98,12 @@ public readonly struct ModMeta(Mod mod) : ISavable mod.Website = newWebsite; } + if (!mod.DefaultPreferredItems.SetEquals(defaultItems)) + { + changes |= ModDataChangeType.DefaultChangedItems; + mod.DefaultPreferredItems = defaultItems; + } + if (newFileVersion != FileVersion) if (ModMigration.Migrate(creator, editor.SaveService, mod, json, ref newFileVersion)) { diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index f97e4d51..700f1d66 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -10,6 +10,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.String; namespace Penumbra.UI.ModsTab; @@ -18,7 +19,8 @@ public class ModPanelChangedItemsTab( ModFileSystemSelector selector, ChangedItemDrawer drawer, ImGuiCacheService cacheService, - Configuration config) + Configuration config, + ModDataEditor dataEditor) : ITab, IUiService { private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); @@ -77,7 +79,7 @@ public class ModPanelChangedItemsTab( => new() { Child = true, - Text = ByteString.FromStringUnsafe(data.ToName(text), false), + Text = ByteString.FromStringUnsafe(data.ToName(text), false), ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), Icon = data.GetIcon().ToFlag(), Expandable = false, @@ -93,7 +95,11 @@ public class ModPanelChangedItemsTab( public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter, ChangedItemMode mode) { - if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset && _lastMode == mode) + if (mod == _lastSelected + && _lastSelected!.LastChangedItemsUpdate == _lastUpdate + && _filter == filter + && !_reset + && _lastMode == mode) return; _reset = false; @@ -138,6 +144,12 @@ public class ModPanelChangedItemsTab( { list.Sort((i1, i2) => { + // reversed + var preferred = _lastSelected.PreferredChangedItems.Contains(i2.Item.Id) + .CompareTo(_lastSelected.PreferredChangedItems.Contains(i1.Item.Id)); + if (preferred != 0) + return preferred; + // reversed var count = i2.Count.CompareTo(i1.Count); if (count != 0) @@ -217,6 +229,7 @@ public class ModPanelChangedItemsTab( .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + using var color = ImRaii.PushColor(ImGuiCol.Button, 0); using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); @@ -241,7 +254,6 @@ public class ModPanelChangedItemsTab( ImGui.TableNextColumn(); if (obj.Expandable) { - using var color = ImRaii.PushColor(ImGuiCol.Button, 0); if (ImUtf8.IconButton(obj.Expanded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight, obj.Expanded ? "Hide the other items using the same model." : obj.Children > 1 ? $"Show {obj.Children} other items using the same model." : @@ -253,6 +265,10 @@ public class ModPanelChangedItemsTab( cache.Reset(); } } + else if (obj is { Child: true, Data: IdentifiedItem item }) + { + DrawPreferredButton(item, idx); + } else { ImGui.Dummy(_buttonSize); @@ -267,6 +283,53 @@ public class ModPanelChangedItemsTab( DrawBaseContainer(obj, idx); } + private void DrawPreferredButton(IdentifiedItem item, int idx) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Star, "Prefer displaying this item instead of the current primary item.\n\nRight-click for more options."u8, _buttonSize, + false, ImGui.GetColorU32(ImGuiCol.TextDisabled, 0.1f))) + dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, false, true); + using var context = ImUtf8.PopupContextItem("StarContext"u8, ImGuiPopupFlags.MouseButtonRight); + if (!context) + return; + + if (cacheService.TryGetCache(_cacheId, out var cache)) + for (--idx; idx >= 0; --idx) + { + if (!cache.Data[idx].Expanded) + continue; + + if (cache.Data[idx].Data is IdentifiedItem it) + { + if (selector.Selected!.PreferredChangedItems.Contains(it.Item.Id) + && ImUtf8.MenuItem("Remove Parent from Local Preferred Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, false); + if (selector.Selected!.DefaultPreferredItems.Contains(it.Item.Id) + && ImUtf8.MenuItem("Remove Parent from Default Preferred Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, true); + } + + break; + } + + var enabled = !selector.Selected!.DefaultPreferredItems.Contains(item.Item.Id); + if (enabled) + { + if (ImUtf8.MenuItem("Add to Local and Default Preferred Changed Items"u8)) + dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, true, true); + } + else + { + if (ImUtf8.MenuItem("Remove from Default Preferred Changed Items"u8)) + dataEditor.RemovePreferredItem(selector.Selected!, item.Item.Id, true); + } + + if (ImUtf8.MenuItem("Reset Local Preferred Items to Default"u8)) + dataEditor.ResetPreferredItems(selector.Selected!); + + if (ImUtf8.MenuItem("Clear Local and Default Preferred Items not Changed by the Mod"u8)) + dataEditor.ClearInvalidPreferredItems(selector.Selected!); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx) diff --git a/schemas/local_mod_data-v3.json b/schemas/local_mod_data-v3.json index bf5d1311..c50e130e 100644 --- a/schemas/local_mod_data-v3.json +++ b/schemas/local_mod_data-v3.json @@ -26,6 +26,15 @@ "Favorite": { "description": "Whether the mod is favourited by the user.", "type": "boolean" + }, + "PreferredChangedItems": { + "description": "Preferred items to list as the main item of a group.", + "type": "array", + "items": { + "minimum": 0, + "type": "integer" + }, + "uniqueItems": true } }, "required": [ "FileVersion" ] diff --git a/schemas/mod_meta-v3.json b/schemas/mod_meta-v3.json index a926b49e..ed63a228 100644 --- a/schemas/mod_meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -43,6 +43,15 @@ "minLength": 1 }, "uniqueItems": true + }, + "DefaultPreferredItems": { + "description": "Default preferred items to list as the main item of a group managed by the mod creator.", + "type": "array", + "items": { + "minimum": 0, + "type": "integer" + }, + "uniqueItems": true } }, "required": [ From cda9b1df655bb4aceed78f47a1a459b35cdc3692 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Mar 2025 22:34:50 +0100 Subject: [PATCH 612/865] Fix weapon identification bug. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 955c4e6b..a21c1467 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4 +Subproject commit a21c146790b370bd58b0f752385ae153f7e769c0 From 34d51b66aa53d3245e1c37960878e0492f9fdbcf Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 1 Mar 2025 21:37:03 +0000 Subject: [PATCH 613/865] [CI] Updating repo.json for testing_1.3.4.6 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0b8d89cf..b098593a 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.4.0", - "TestingAssemblyVersion": "1.3.4.5", + "TestingAssemblyVersion": "1.3.4.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0b0c92eb098b50d4631fb0ba2d65db5f01273247 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 2 Mar 2025 14:27:40 +0100 Subject: [PATCH 614/865] Some cleanup. --- Penumbra/Import/Structs/TexToolsStructs.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Structs/TexToolsStructs.cs b/Penumbra/Import/Structs/TexToolsStructs.cs index bf3bccd8..f5b5ef4a 100644 --- a/Penumbra/Import/Structs/TexToolsStructs.cs +++ b/Penumbra/Import/Structs/TexToolsStructs.cs @@ -26,7 +26,7 @@ public class SimpleMod public class ModPackPage { public int PageIndex = 0; - public ModGroup[] ModGroups = Array.Empty(); + public ModGroup[] ModGroups = []; } [Serializable] @@ -34,7 +34,7 @@ public class ModGroup { public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; - public OptionList[] OptionList = Array.Empty(); + public OptionList[] OptionList = []; public string Description = string.Empty; } @@ -44,7 +44,7 @@ public class OptionList public string Name = string.Empty; public string Description = string.Empty; public string ImagePath = string.Empty; - public SimpleMod[] ModsJsons = Array.Empty(); + public SimpleMod[] ModsJsons = []; public string GroupName = string.Empty; public GroupType SelectionType = GroupType.Single; public bool IsChecked = false; @@ -59,8 +59,8 @@ public class ExtendedModPack public string Version = string.Empty; public string Description = DefaultTexToolsData.Description; public string Url = string.Empty; - public ModPackPage[] ModPackPages = Array.Empty(); - public SimpleMod[] SimpleModsList = Array.Empty(); + public ModPackPage[] ModPackPages = []; + public SimpleMod[] SimpleModsList = []; } [Serializable] @@ -72,5 +72,5 @@ public class SimpleModPack public string Version = string.Empty; public string Description = DefaultTexToolsData.Description; public string Url = string.Empty; - public SimpleMod[] SimpleModsList = Array.Empty(); + public SimpleMod[] SimpleModsList = []; } From 7cf0367361934ae063c8b5c6d826235a554a39e1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 13:39:25 +0100 Subject: [PATCH 615/865] Try moving extracted folders 3 times for unknown issues. --- Penumbra/Import/TexToolsImporter.Archives.cs | 39 +++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index febbe179..8166dea7 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -96,17 +96,36 @@ public partial class TexToolsImporter _token.ThrowIfCancellationRequested(); var oldName = _currentModDirectory.FullName; - // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. - if (leadDir) + + // Try renaming the folder three times because sometimes we get AccessDenied here for some unknown reason. + const int numTries = 3; + for (var i = 1;; ++i) { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false); - Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); - Directory.Delete(oldName); - } - else - { - _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); - Directory.Move(oldName, _currentModDirectory.FullName); + // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. + try + { + if (leadDir) + { + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, baseName, _config.ReplaceNonAsciiOnImport, false); + Directory.Move(Path.Combine(oldName, baseName), _currentModDirectory.FullName); + Directory.Delete(oldName); + } + else + { + _currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, name, _config.ReplaceNonAsciiOnImport, false); + Directory.Move(oldName, _currentModDirectory.FullName); + } + } + catch (IOException io) + { + if (i == numTries) + throw; + + Penumbra.Log.Warning($"Error when renaming the extracted mod, try {i}/{numTries}: {io.Message}."); + continue; + } + + break; } _currentModDirectory.Refresh(); From 1afbbfef78f7ec295bc5ccb7cfd15b6a23b74eae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 13:39:41 +0100 Subject: [PATCH 616/865] Update NuGet packages. --- Penumbra/packages.lock.json | 87 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 5b868212..9aa1ebd5 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -13,67 +13,68 @@ }, "PeNet": { "type": "Direct", - "requested": "[4.0.5, )", - "resolved": "4.0.5", - "contentHash": "/OUfRnMG8STVuK8kTpdfm+WQGTDoUiJnI845kFw4QrDmv2gYourmSnH84pqVjHT1YHBSuRfCzfioIpHGjFJrGA==", + "requested": "[4.1.1, )", + "resolved": "4.1.1", + "contentHash": "TiRyOVcg1Bh2FyP6Dm2NEiYzemSlQderhaxuH3XWNyTYsnHrm1n/xvoTftgMwsWD4C/3kTqJw93oZOvHojJfKg==", "dependencies": { "PeNet.Asn1": "2.0.1", - "System.Security.Cryptography.Pkcs": "8.0.0" + "System.Security.Cryptography.Pkcs": "8.0.1" } }, "SharpCompress": { "type": "Direct", - "requested": "[0.37.2, )", - "resolved": "0.37.2", - "contentHash": "cFBpTct57aubLQXkdqMmgP8GGTFRh7fnRWP53lgE/EYUpDZJ27SSvTkdjB4OYQRZ20SJFpzczUquKLbt/9xkhw==", + "requested": "[0.39.0, )", + "resolved": "0.39.0", + "contentHash": "0esqIUDlg68Z7+Weuge4QzEvNtawUO4obTJFL7xuf4DBHMxVRr+wbNgiX9arMrj3kGXQSvLe0zbZG3oxpkwJOA==", "dependencies": { - "ZstdSharp.Port": "0.8.0" + "System.Buffers": "4.6.0", + "ZstdSharp.Port": "0.8.4" } }, "SharpGLTF.Core": { "type": "Direct", - "requested": "[1.0.1, )", - "resolved": "1.0.1", - "contentHash": "ykeV1oNHcJrEJE7s0pGAsf/nYGYY7wqF9nxCMxJUjp/WdW+UUgR1cGdbAa2lVZPkiXEwLzWenZ5wPz7yS0Gj9w==" + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "su+Flcg2g6GgOIgulRGBDMHA6zY5NBx6NYH1Ayd6iBbSbwspHsN2VQgZfANgJy92cBf7qtpjC0uMiShbO+TEEg==" }, "SharpGLTF.Toolkit": { "type": "Direct", - "requested": "[1.0.1, )", - "resolved": "1.0.1", - "contentHash": "LYBjHdHW5Z8R1oT1iI04si3559tWdZ3jTdHfDEu0jqhuyU8w3oJRLFUoDfVeCOI5zWXlVQPtlpjhH9XTfFFAcA==", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vkEuf8ch76NNgZXU/3zoXTIXRO0o14H3aRoSFzcuUQb0PTxvV6jEfmWkUVO6JtLDuFCIimqZaf3hdxr32ltpfQ==", "dependencies": { - "SharpGLTF.Runtime": "1.0.1" + "SharpGLTF.Runtime": "1.0.3" } }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" + "requested": "[3.1.7, )", + "resolved": "3.1.7", + "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" }, "System.Formats.Asn1": { "type": "Direct", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "OKWHCPYQr/+cIoO8EVjFn7yFyiT8Mnf1wif/5bYGsqxQV6PrwlX2HQ9brZNx57ViOvRe4ing1xgHCKl/5Ko8xg==" }, "JetBrains.Annotations": { "type": "Transitive", - "resolved": "2024.2.0", - "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "resolved": "9.0.2", + "contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" + "resolved": "9.0.2", + "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" }, "PeNet.Asn1": { "type": "Transitive", @@ -82,19 +83,21 @@ }, "SharpGLTF.Runtime": { "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "KsgEBKLfsEnu2IPeKaWp4Ih97+kby17IohrAB6Ev8gET18iS80nKMW/APytQWpenMmcWU06utInpANqyrwRlDg==", + "resolved": "1.0.3", + "contentHash": "W0bg2WyXlcSAJVu153hNUNm+BU4RP46yLwGD4099hSm8dsXG/H+J95PBoLJbIq8KGVkUWvfM0+XWHoEkCyd50A==", "dependencies": { - "SharpGLTF.Core": "1.0.1" + "SharpGLTF.Core": "1.0.3" } }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ULmp3xoOwNYjOYp4JZ2NK/6NdTgiN1GQXzVVN1njQ7LOZ0d0B9vyMnhyqbIi9Qw4JXj1JgCsitkTShboHRx7Eg==", - "dependencies": { - "System.Formats.Asn1": "8.0.0" - } + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" }, "System.ValueTuple": { "type": "Transitive", @@ -111,14 +114,14 @@ }, "ZstdSharp.Port": { "type": "Transitive", - "resolved": "0.8.0", - "contentHash": "Z62eNBIu8E8YtbqlMy57tK3dV1+m2b9NhPeaYovB5exmLKvrGCqOhJTzrEUH5VyUWU6vwX3c1XHJGhW5HVs8dA==" + "resolved": "0.8.4", + "contentHash": "eieSXq3kakCUXbgdxkKaRqWS6hF0KBJcqok9LlDCs60GOyrynLvPOcQ0pRw7shdPF7lh/VepJ9cP9n9HHc759g==" }, "ottergui": { "type": "Project", "dependencies": { - "JetBrains.Annotations": "[2024.2.0, )", - "Microsoft.Extensions.DependencyInjection": "[8.0.0, )" + "JetBrains.Annotations": "[2024.3.0, )", + "Microsoft.Extensions.DependencyInjection": "[9.0.2, )" } }, "penumbra.api": { @@ -131,8 +134,8 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.3.0, )", - "Penumbra.String": "[1.0.4, )" + "Penumbra.Api": "[5.6.0, )", + "Penumbra.String": "[1.0.5, )" } }, "penumbra.string": { From 6eacc82dcdb5bb26f54421de239ea0be4eec9750 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 13:48:41 +0100 Subject: [PATCH 617/865] Update references. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Penumbra.csproj | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index c347d29d..13f1a90b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf +Subproject commit 13f1a90b88d2b8572480748a209f957b70d6a46f diff --git a/Penumbra.Api b/Penumbra.Api index 70f04683..404c8aaa 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 70f046830cc7cd35b3480b12b7efe94182477fbb +Subproject commit 404c8aaa5115925006963baa118bf710c7953380 diff --git a/Penumbra.GameData b/Penumbra.GameData index a21c1467..96163f79 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a21c146790b370bd58b0f752385ae153f7e769c0 +Subproject commit 96163f79e13c7d52cc36cdd82ab4e823763f4f31 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 9b613729..b4266aeb 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -87,13 +87,13 @@ - + - - - - - + + + + + From eab98ec0e4cefaeff12fccb55a6fdc5c36cbfde3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 9 Mar 2025 14:41:45 +0100 Subject: [PATCH 618/865] 1.3.5.0 --- Penumbra/UI/Changelog.cs | 135 ++++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 993ace62..87dd101d 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -58,20 +58,65 @@ public class PenumbraChangelog : IUiService Add1_3_2_0(Changelog); Add1_3_3_0(Changelog); Add1_3_4_0(Changelog); - } - + Add1_3_5_0(Changelog); + } + #region Changelogs + private static void Add1_3_5_0(Changelog log) + => log.NextVersion("Version 1.3.5.0") + .RegisterImportant( + "Redirections of unsupported file types like .atch will now produce warnings when they are enabled. Please update mods still containing them or request updates from their creators.") + .RegisterEntry("You can now import .atch in the Meta section of advanced editing to add their non-default changes to the mod.") + .RegisterHighlight("Added an option in settings and in the collection bar in the mod tab to always use temporary settings.") + .RegisterEntry( + "While this option is enabled, all changes you make in the current collection will be applied as temporary changes, and you have to use Turn Permanent to make them permanent.", + 1) + .RegisterEntry( + "This should be useful for trying out new mods without needing to reset their settings later, or for creating mod associations in Glamourer from them.", + 1) + .RegisterEntry( + "Added a context menu entry on the mod selector blank-space context menu to clear all temporary settings made manually.") + .RegisterHighlight( + "Resource Trees now consider some additional files like decals, and improved the quick-import behaviour for some files that should not generally be modded.") + .RegisterHighlight("The Changed Item display for single mods has been heavily improved.") + .RegisterEntry("Any changed item will now show how many individual edits are affecting it in the mod in its tooltip.", 1) + .RegisterEntry("Equipment pieces are now grouped by their model id, reducing clutter.", 1) + .RegisterEntry( + "The primary equipment piece displayed is the one with the most changes affecting it, but can be configured to a specific item by the mod creator and locally.", + 1) + .RegisterEntry( + "Preferred changed items stored in the mod will be shared when exporting the mod, and used as the default for local preferences, which will not be shared.", + 2) + .RegisterEntry( + "You can configure whether groups are automatically collapsed or expanded, or remove grouping entirely in the settings.", 1) + .RegisterHighlight("Fixed support for model import/export with more than one UV.") + .RegisterEntry("Added some IPC relating to changed items.") + .RegisterEntry("Skeleton and Physics changes should now be identified in Changed Items.") + .RegisterEntry("Item Swaps will now also correctly swap EQP entries of multi-slot pieces.") + .RegisterEntry("Meta edit transmission through IPC should be a lot more efficient than before.") + .RegisterEntry("Fixed an issue with incognito names in some cutscenes.") + .RegisterEntry("Newly extracted mod folders will now try to rename themselves three times before being considered a failure."); + private static void Add1_3_4_0(Changelog log) => log.NextVersion("Version 1.3.4.0") - .RegisterHighlight("Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.") - .RegisterEntry("This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", 1) + .RegisterHighlight( + "Added HDR functionality to diffuse buffers. This allows more accurate representation of non-standard color values for e.g. skin or hair colors when used with advanced customizations in Glamourer.") + .RegisterEntry( + "This option requires Wait For Plugins On Load to be enabled in Dalamud and to be enabled on start to work. It is on by default but can be turned off.", + 1) .RegisterHighlight("Added a new option group type: Combining Groups.") - .RegisterEntry("A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", 1) - .RegisterEntry("Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", 1) - .RegisterEntry("Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.") + .RegisterEntry( + "A combining group behaves similarly to a multi group for the user, but instead of enabling the different options separately, it results in exactly one option per choice of settings.", + 1) + .RegisterEntry( + "Example: The user sees 2 checkboxes [+25%, +50%], but the 4 different selection states result in +0%, +25%, +50% or +75% if both are toggled on. Every choice of settings can be configured separately by the mod creator.", + 1) + .RegisterEntry( + "Added new functionality to better track copies of the player character in cutscenes if they get forced to specific clothing, like in the Margrat cutscene. Might improve tracking in wedding ceremonies, too, let me know.") .RegisterEntry("Added a display of the number of selected files and folders to the multi mod selection.") - .RegisterEntry("Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.") + .RegisterEntry( + "Added cleaning functionality to remove outdated or unused files or backups from the config and mod folders via manual action.") .RegisterEntry("Updated the Bone and Material limits in the Model Importer.") .RegisterEntry("Improved handling of IMC and Material files loaded asynchronously.") .RegisterEntry("Added IPC functionality to query temporary settings.") @@ -86,49 +131,75 @@ public class PenumbraChangelog : IUiService private static void Add1_3_3_0(Changelog log) => log.NextVersion("Version 1.3.3.0") .RegisterHighlight("Added Temporary Settings to collections.") - .RegisterEntry("Settings can be manually turned temporary (and turned back) while editing mod settings via right-click context on the mod or buttons in the settings panel.", 1) - .RegisterEntry("This can be used to test mods or changes without saving those changes permanently or having to reinstate the old settings afterwards.", 1) - .RegisterEntry("More importantly, this can be set via IPC by other plugins, allowing Glamourer to only set and reset temporary settings when applying Mod Associations.", 1) - .RegisterEntry("As an extreme example, it would be possible to only enable the consistent mods for your character in the collection, and let Glamourer handle all outfit mods itself via temporary settings only.", 1) - .RegisterEntry("This required some pretty big changes that were in testing for a while now, but nobody talked about it much so it may still have some bugs or usability issues. Let me know!", 1) - .RegisterHighlight("Added an option to automatically select the collection assigned to the current character on login events. This is off by default.") - .RegisterEntry("Added partial copying of color tables in material editing via right-click context menu entries on the import buttons.") - .RegisterHighlight("Added handling for TMB files cached by the game that should resolve issues of leaky TMBs from animation and VFX mods.") - .RegisterEntry("The enabled checkbox, Priority and Inheriting buttons now stick at the top of the Mod Settings panel even when scrolling down for specific settings.") + .RegisterEntry( + "Settings can be manually turned temporary (and turned back) while editing mod settings via right-click context on the mod or buttons in the settings panel.", + 1) + .RegisterEntry( + "This can be used to test mods or changes without saving those changes permanently or having to reinstate the old settings afterwards.", + 1) + .RegisterEntry( + "More importantly, this can be set via IPC by other plugins, allowing Glamourer to only set and reset temporary settings when applying Mod Associations.", + 1) + .RegisterEntry( + "As an extreme example, it would be possible to only enable the consistent mods for your character in the collection, and let Glamourer handle all outfit mods itself via temporary settings only.", + 1) + .RegisterEntry( + "This required some pretty big changes that were in testing for a while now, but nobody talked about it much so it may still have some bugs or usability issues. Let me know!", + 1) + .RegisterHighlight( + "Added an option to automatically select the collection assigned to the current character on login events. This is off by default.") + .RegisterEntry( + "Added partial copying of color tables in material editing via right-click context menu entries on the import buttons.") + .RegisterHighlight( + "Added handling for TMB files cached by the game that should resolve issues of leaky TMBs from animation and VFX mods.") + .RegisterEntry( + "The enabled checkbox, Priority and Inheriting buttons now stick at the top of the Mod Settings panel even when scrolling down for specific settings.") .RegisterEntry("When creating new mods with Item Swap, the attributed author of the resulting mod was improved.") .RegisterEntry("Fixed an issue with rings in the On-Screen tab and in the data sent over to other plugins via IPC.") - .RegisterEntry("Fixed some issues when writing material files that resulted in technically valid files that still caused some issues with the game for unknown reasons.") + .RegisterEntry( + "Fixed some issues when writing material files that resulted in technically valid files that still caused some issues with the game for unknown reasons.") .RegisterEntry("Fixed some ImGui assertions."); private static void Add1_3_2_0(Changelog log) => log.NextVersion("Version 1.3.2.0") .RegisterHighlight("Added ATCH meta manipulations that allow the composite editing of attachment points across multiple mods.") .RegisterEntry("Those ATCH manipulations should be shared via Mare Synchronos.", 1) - .RegisterEntry("This is an early implementation and might be bug-prone. Let me know of any issues. It was in testing for quite a while without reports.", 1) - .RegisterEntry("Added jumping to identified mods in the On-Screen tab via Control + Right-Click and improved their display slightly.") + .RegisterEntry( + "This is an early implementation and might be bug-prone. Let me know of any issues. It was in testing for quite a while without reports.", + 1) + .RegisterEntry( + "Added jumping to identified mods in the On-Screen tab via Control + Right-Click and improved their display slightly.") .RegisterEntry("Added some right-click context menu copy options in the File Redirections editor for paths.") .RegisterHighlight("Added the option to change a specific mod's settings via chat commands by using '/penumbra mod settings'.") .RegisterEntry("Fixed issues with the copy-pasting of meta manipulations.") .RegisterEntry("Fixed some other issues related to meta manipulations.") - .RegisterEntry("Updated available NPC names and fixed an issue with some supposedly invisible characters in names showing in ImGui."); + .RegisterEntry( + "Updated available NPC names and fixed an issue with some supposedly invisible characters in names showing in ImGui."); private static void Add1_3_1_0(Changelog log) => log.NextVersion("Version 1.3.1.0") .RegisterEntry("Penumbra has been updated for Dalamud API 11 and patch 7.1.") - .RegisterImportant("There are some known issues with potential crashes using certain VFX/SFX mods, probably related to sound files.") - .RegisterEntry("If you encounter those issues, please report them in the discord and potentially disable the corresponding mods for the time being.", 1) - .RegisterImportant("The modding of .atch files has been disabled. Outdated modded versions of these files cause crashes when loaded.") + .RegisterImportant( + "There are some known issues with potential crashes using certain VFX/SFX mods, probably related to sound files.") + .RegisterEntry( + "If you encounter those issues, please report them in the discord and potentially disable the corresponding mods for the time being.", + 1) + .RegisterImportant( + "The modding of .atch files has been disabled. Outdated modded versions of these files cause crashes when loaded.") .RegisterEntry("A better way for modular modding of .atch files via meta changes will release to the testing branch soonish.", 1) .RegisterHighlight("Temporary collections (as created by Mare) will now always respect ownership.") - .RegisterEntry("This means that you can toggle this setting off if you do not want it, and Mare will still work for minions and mounts of other players.", 1) - .RegisterEntry("The new physics and animation engine files (.kdb and .bnmb) should now be correctly redirected and respect EST changes.") + .RegisterEntry( + "This means that you can toggle this setting off if you do not want it, and Mare will still work for minions and mounts of other players.", + 1) + .RegisterEntry( + "The new physics and animation engine files (.kdb and .bnmb) should now be correctly redirected and respect EST changes.") .RegisterEntry("Fixed issues with EQP entries being labeled wrongly and global EQP not changing all required values for earrings.") .RegisterEntry("Fixed an issue with global EQP changes of a mod being reset upon reloading the mod.") .RegisterEntry("Fixed another issue with left rings and mare synchronization / the on-screen tab.") .RegisterEntry("Maybe fixed some issues with characters appearing in the login screen being misidentified.") .RegisterEntry("Some improvements for debug visualization have been made."); - + private static void Add1_3_0_0(Changelog log) => log.NextVersion("Version 1.3.0.0") @@ -138,16 +209,20 @@ public class PenumbraChangelog : IUiService .RegisterEntry("Reworked quite a bit of things around face wear / bonus items. Please let me know if anything broke.", 1) .RegisterEntry("The import date of a mod is now shown in the Edit Mod tab, and can be reset via button.") .RegisterEntry("A button to open the file containing local mod data for a mod was also added.", 1) - .RegisterHighlight("IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") + .RegisterHighlight( + "IMC groups can now be configured to only apply the attribute flags for their entry, and take the other values from the default value.") .RegisterEntry("This allows keeping the material index of every IMC entry of a group, while setting the attributes.", 1) .RegisterHighlight("Model Import/Export was fixed and re-enabled (thanks ackwell and ramen).") .RegisterHighlight("Added a hack to allow bonus items (face wear, glasses) to have VFX.") .RegisterEntry("Also fixed the hack that allowed accessories to have VFX not working anymore.", 1) .RegisterHighlight("Added rudimentary options to edit PBD files in the advanced editing window.") .RegisterEntry("Preparing the advanced editing window for a mod now does not freeze the game until it is ready.") - .RegisterEntry("Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") + .RegisterEntry( + "Meta Manipulations in the advanced editing window are now ordered and do not eat into performance as much when drawn.") .RegisterEntry("Added a button to the advanced editing window to remove all default-valued meta manipulations from a mod") - .RegisterEntry("Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", 1) + .RegisterEntry( + "Default-valued manipulations will now also be removed on import from archives and .pmps, not just .ttmps, if not configured otherwise.", + 1) .RegisterEntry("Checkbox-based mod filters are now tri-state checkboxes instead of two disjoint checkboxes.") .RegisterEntry("Paths from the resource logger can now be copied.") .RegisterEntry("Silenced some redundant error logs when updating mods via Heliosphere.") From 1d70be8060f92e3c7ad9fdfd671a42b7437774bf Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 9 Mar 2025 22:16:58 +0000 Subject: [PATCH 619/865] [CI] Updating repo.json for 1.3.5.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index b098593a..8de11f0e 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.3.4.0", - "TestingAssemblyVersion": "1.3.4.6", + "AssemblyVersion": "1.3.5.0", + "TestingAssemblyVersion": "1.3.5.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.4.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.4.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 93b0996794ea68104cc0f93bfb9ab826a91ec318 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 11 Mar 2025 18:12:54 +0100 Subject: [PATCH 620/865] Add chat command to clear temporary settings. --- Penumbra/CommandHandler.cs | 47 ++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index dee46e32..9f681da2 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -75,20 +75,21 @@ public class CommandHandler : IDisposable, IApiService _ = argumentList[0].ToLowerInvariant() switch { - "window" => ToggleWindow(arguments), - "enable" => SetPenumbraState(arguments, true), - "disable" => SetPenumbraState(arguments, false), - "toggle" => SetPenumbraState(arguments, null), - "reload" => Reload(arguments), - "redraw" => Redraw(arguments), - "lockui" => SetUiLockState(arguments), - "size" => SetUiMinimumSize(arguments), - "debug" => SetDebug(arguments), - "collection" => SetCollection(arguments), - "mod" => SetMod(arguments), - "bulktag" => SetTag(arguments), - "knowledge" => HandleKnowledge(arguments), - _ => PrintHelp(argumentList[0]), + "window" => ToggleWindow(arguments), + "enable" => SetPenumbraState(arguments, true), + "disable" => SetPenumbraState(arguments, false), + "toggle" => SetPenumbraState(arguments, null), + "reload" => Reload(arguments), + "redraw" => Redraw(arguments), + "lockui" => SetUiLockState(arguments), + "size" => SetUiMinimumSize(arguments), + "debug" => SetDebug(arguments), + "collection" => SetCollection(arguments), + "mod" => SetMod(arguments), + "bulktag" => SetTag(arguments), + "clearsettings" => ClearSettings(arguments), + "knowledge" => HandleKnowledge(arguments), + _ => PrintHelp(argumentList[0]), }; } @@ -126,6 +127,21 @@ public class CommandHandler : IDisposable, IApiService _chat.Print(new SeStringBuilder() .AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.") .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("clearsettings", + "Clear all temporary settings applied manually through Penumbra in the current or all collections. Use with 'all' parameter for all.") + .BuiltString); + return true; + } + + private bool ClearSettings(string arguments) + { + if (arguments.Trim().ToLowerInvariant() is "all") + foreach (var collection in _collectionManager.Storage) + _collectionEditor.ClearTemporarySettings(collection); + else + _collectionEditor.ClearTemporarySettings(_collectionManager.Active.Current); + return true; } @@ -416,7 +432,8 @@ public class CommandHandler : IDisposable, IApiService var split2 = nameSplit[1].Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (split2.Length < 2) { - _chat.Print("Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options."); + _chat.Print( + "Not enough arguments for changing settings provided. Please add a group name and a list of setting names - which can be empty for multi options."); return false; } From e5620e17e0f468ab6331e9dbf347e881c592cb5b Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 01:20:36 +0100 Subject: [PATCH 621/865] Improve texture saving --- Penumbra/Import/Textures/TextureManager.cs | 43 ++++++++++++------ Penumbra/Penumbra.csproj | 4 ++ .../Materials/MtrlTab.LivePreview.cs | 2 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 21 +++++++-- Penumbra/lib/OtterTex.dll | Bin 41984 -> 42496 bytes 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 7118f8af..6adf5861 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; @@ -6,15 +7,17 @@ using OtterGui.Log; using OtterGui.Services; using OtterGui.Tasks; using OtterTex; +using SharpDX.Direct3D11; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; +using DxgiDevice = SharpDX.DXGI.Device; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; -public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider) +public sealed class TextureManager(IDataManager gameData, Logger logger, ITextureProvider textureProvider, IUiBuilder uiBuilder) : SingleTaskQueue, IDisposable, IService { private readonly Logger _logger = logger; @@ -47,11 +50,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) - => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); + => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, input, output)); public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); + => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, image, path, rgba, width, height)); private Task Enqueue(IAction action) { @@ -156,27 +159,30 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur private readonly string _outputPath; private readonly ImageInputData _input; private readonly CombinedTexture.TextureSaveType _type; + private readonly Device? _device; private readonly bool _mipMaps; private readonly bool _asTex; - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, - string output) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, + string input, string output) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; _type = type; + _device = device; _mipMaps = mipMaps; _asTex = asTex; } - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, - string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, + BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; _type = type; + _device = device; _mipMaps = mipMaps; _asTex = asTex; } @@ -201,8 +207,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, _device, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, _device, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -320,8 +326,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, - int width = 0, int height = 0) + public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel, + byte[]? rgba = null, int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) { @@ -331,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, cancel); + return CreateCompressed(dds, mipMaps, bc7, device, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, cancel); + return CreateCompressed(scratch, mipMaps, bc7, device, cancel); } default: return new BaseImage(); } @@ -384,7 +390,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel) { var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) @@ -398,6 +404,15 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur input = AddMipMaps(input, mipMaps); cancel.ThrowIfCancellationRequested(); + // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. + if (device is not null && format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) + { + var dxgiDevice = device.QueryInterface(); + + using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); + return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel); + } + return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index b4266aeb..870865da 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -80,6 +80,10 @@ $(DalamudLibPath)SharpDX.Direct3D11.dll False + + $(DalamudLibPath)SharpDX.DXGI.dll + False + lib\OtterTex.dll diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 01a40980..5025bafd 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -138,7 +138,7 @@ public partial class MtrlTab foreach (var constant in Mtrl.ShaderPackage.Constants) { var values = Mtrl.GetConstantValue(constant); - if (values != null) + if (values != []) SetMaterialParameter(constant.Id, 0, values); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index c08e8a8e..d0764808 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -134,7 +134,7 @@ public partial class ModEditWindow tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -159,7 +159,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -169,7 +169,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } @@ -180,7 +180,7 @@ public partial class ModEditWindow || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); - InvokeChange(Mod, _left.Path); + AddChangeTask(_left.Path); AddReloadTask(_left.Path, false); } } @@ -235,7 +235,7 @@ public partial class ModEditWindow if (a) { _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); - InvokeChange(Mod, b); + AddChangeTask(b); if (b == _left.Path) AddReloadTask(_left.Path, false); else if (b == _right.Path) @@ -245,6 +245,17 @@ public partial class ModEditWindow _forceTextureStartPath = false; } + private void AddChangeTask(string path) + { + _center.SaveTask.ContinueWith(t => + { + if (!t.IsCompletedSuccessfully) + return; + + _framework.RunOnFrameworkThread(() => InvokeChange(Mod, path)); + }, TaskScheduler.Default); + } + private void AddReloadTask(string path, bool right) { _center.SaveTask.ContinueWith(t => diff --git a/Penumbra/lib/OtterTex.dll b/Penumbra/lib/OtterTex.dll index 29912e6215ff28a7959b73aa1e00cc9ebb919d76..c137aee184d789102648a05ecc24c0437df54ba1 100644 GIT binary patch delta 16738 zcmcgz33wD$wm!GI)BBpWvsYF+Awa?sHkDQoK|w);Vbu;vfQ+Clg5W|mg6OEo#KH)S zKqD|3gh36s45BT9qJkp>ZZo2-f}(<=gN`W5JLgn&C4#8$&3iBT`mgi<=iGDGx>eOn zWS11#E&cKK^o(ZvN1*>5gF@LURRTmCS1wO_=ox?>>X$3eQRj#Z$GNL8NXA!GT`t;- z2hy{aFvLEs=Hj>#5?MbuI}OhK1dMY(tU6FjD*roPK38>#E_Z2Up^*iyhdLQB|>K7@a#BOz9>KL&{U6(pa zyox18sjl?w3>+bgGg(-ICV=+kpPOdXqcGI0`cn$pCwa`CB-4s%?S)-!%Iw%aTO|4> zK{BSy)cO{5qRxY3rokzcOu-!Wi_A3PR+F+SMXtI&t6b!%HCX}mXqHF4Evv91U(1vj z0zHu_nDUcMrIOhcl&PRdQ$NQf1!`t?k$Nz@mpUsuQxvOz%&rh6>bC55qEtOMyTZ{9 zi)-Ch^RshBkVH|YPRQwa?)?`66wL*&^#=&y1QiXtZhFHt6K7yIV8B#>Iy@E)9|jxH zrbSeA(eQ!&@c1o2(+NAX-IMJRjG3%#Nh7b6PVtAh z)CWrL_k|X?DV=a!T3NXT+NV(kZ)B{>#)?Y_uTLZVG@Wo*F5xAmgn#1(O?0lxc0hf` z3M|z>Eu3$G!Q8}q8JDC|WN(bIK-WCN0USRplj4^;6_Q>FQoAbI@hLhS@Nqu5*C&%V zE1m2(Hpl_@XOTTGlkC~~LF~s7tiS{R^k$&FmMc5NrOa8kx*c#JNb&VD^rW)r!yP-0>p6i-ovEzP=?Ho^hXa>Q7*oL`IvJa?qT)NMU)FCvUfcI^PnrT zQaDmjXe|XLIkOZG8*m3hRi-;?z!vliY;yOu{*A-^@5VifC;Y}mSmY)gkC+I$u;@Hgj(Q2V z`v|WsBph2pc%1c@^2okDNVqGR@C`;QD-->sJd#iNAjdamll`$*p+kF4`ZyQ38-o%R zpO<$5dPkKKZt@b=2MGV1O*kv-f;N%;QMs~+@bMDDcI^l^mlDokeHZIJSl`LIv$(&4 z?yuQ#XAXt_k=@T`ll>6~-O7dI9Mhk@`&i%2`dn5Hu-(9Rl^;EMOKdOTNJ}pD;2l=c zHQOV((PKEN>Zdl^;6yBYT}?q$3xN1@Q|OkQN%%{YY91sK1=fu%Eo_6Do-V#JO>Sj?Ek_<9y8 zixCr%t4l9MCB;Kn%Gi}LmPbk^+ZCQmY5n(N(nlsoE!#8Mp5Y~BW-j5E8HB^~2{*H{ zAdBq$AYl^gJvn|ED-xqUg}gU%whbg^@y!)zZ~5T7vaHF!jZXz zLxO}$P;ZSKL3%#n0LEdA6}hBzFCnbUA{@!ekRaLDxd{)VZc+4sU0pgFrAc1KT*5;c zgws5Po&AKFY*#S$kx99QBX=NHLrvC|)&f}QE3*nH%&I_KP+DQNz=x`B&!iP`uh%7=oF|dnM=c2OAML1a|T;V1hpGUZ) zfY8BSVEmNzMQjhtC-3H5!ptDyDfV{pl6|t>TpWonJhvbgW+{n*5^xz0+O_~Ge=Ve; z`=9`?1u);)T^~jZJdoSVnhtyN7ozfwOfk=6Jl=MA??gQ5UyS&vXS{WQOt&=)1N~*X zwHact<(PYt!B$y?0@oXqoC^(xcQ$Y@v&O)c@;dabVAce`_cbG1r?Vtv5^U7jHgB`l z1Y300Kd;$phHW}4%5SzN!A_m^!*Ui-_UJS`RySA*0RNi6DQ~iRz^gOGwZ-a%3`M8k&_F(%G8K=dEcl**on4B97=%4KtH41lhqrV#6w6h>hdL|5 zvFHe&>rBQhm2g65C$L;+Fw@f<*7rD$VOQ`L0@dFLGqGSbWa-ZJIEXzc4kFMff0uRG z+RI=8`LLB9U^Ji8yTy8r!Cd)Utmhi+6&%C!4AvFLu#dqia12At6sn>b2l0Fe>eauP z`MLE1BXm>257q%XQ{`r}*>Zg@ku!rYMCi?jwZoan8CEkc2{YpZ5-TUuzcH7G48hFfLCAJ!f>8vWRgKa$QVfK-@udoZUII|ZNxUaCMtrn7ssOc}jT>o@Y2X1Cf zHgEZPwmQhySvIl>&{bz8!A#pE2t^p_}p~?0yrEI!w8sNEye~mU#J`0&och2?RXqyiGbk>M$2F%tN>3$kLgB`e<+jA2Ku~A93ok}CKxh3+o<_DoXJ31sL-v%;dIw&%&4U-1 z@x&VUYYPI*;h=nbcQpw}7o3_X+MOdEGW2f;uZne%H1T`sr+P;IQou2s3Mf z6Yei;cR|!(r)-NMW-yoi9ynsK47+kKxPnwg6C88q+wX%x2J2*B4wDUbj(r6zHP}V= zC_JgN2V9rgAAoNSc9ne{1j@9W54a}VAA)`cyWajV)ah)STd{9cAZpM>_Dv8o*i!qW zaKvD%?JdCXTEapQo?*7l(3Re)!@cv-vIp&t!&set7JS^k6&C2ML)lJy3^wTO+Oqxj zov>eLyHnn`zl8T6?X@(?DPP$4KtG)gN%_IP7jDwo-6>AT>#$j8j?@gtTkt-!CR~Wc zjsx(m!Kxf_$f(eI|Dvn6;{)hnu!|jsVT{gBx<)vTLPRm>1jlh$r?WEm2*-D@%U}~6 zLLAlEtG*EqtMGNus(95m!Qm0zbhaxn!;vb67;K&+Ag1bUMer_1ws^>3D;@b_R|mfS zHNmUCjgBG#9TQ2Pag+*KXJzhk?CvCING#~2U;nC+UL+pUod_WT0D#l`@ay52d;2V z7Ke4`@j%!)RoJU2(L(4FY;evH{R}qCdA+FD+2GWO^9FIh!ESfnDE_LmjG{%zjvH*L zbFN73tmPcyUgV63A;c7{Vu*XGbH13WJI_guI&T#l47T2RyV$L>Wu8Z!cL;MAEzL5| zR_9%!NM{F&w>uY!u)+T3Y!veh_O^40h#Bl-=Q8mjGi4!|O1^Tg5IJ47q^6RS&Q+qf z&KBpHTv0LGU_RGcalg(EJ0s2q#W7}$0BKpSheV(oWod-(17)s_qMy!s2Yb0Ti>W#r zpSs_^RV>w*0_}kTng zXQ|G+Tn9u{XP%N(u0vv%&Z2pLaeb<<3st_t5p#WMFr^7%uCENbNWAF!m%*B)*IZv4 zY=P+?t`i3PP`vN@&S2k4$6Tij_NnQV%Pdi&BY1m*pE0=Y2AgWhcDoIBw_+`K`wcqJ z*3+G4uv_c{+?fVj=NRtJF_k>k4;YgB`N1cMmYwVf%XbAcH-h^tgM7!5r3|?%@Wjvc2IRYp?>l$6af%LdRkE zM1ysQ!-{)~K`#;Cx@Q<{mSpwJG1zERx@WGz=9q(?TMc$Bskdj5!8%(n_S_>K=eb{K zquZNh(hoWdWF^5e$=aQ!W}%I4c9i9kPp4B#lVG`&rL*2x@J~{a&Mw4)f08=rY;|C| zxL>N)*``1e+%JW6wz6=#SRoD6*@nU-SRq}Zv$gsCZ7ZdTI-8dFvTdazDLTE_gH*ap zXRG~VJu9Uqoo({hAZyl{f(2Jen>7YC!HJ$#(hg=#VE0_(c|ba1u(_Ufl9L}M$akmb zK`CIcCeM0F>7mn|?v^1HtTF(S*7=7=`EcdNjb;+s$}Dbr$%TjyU4p&s?gbAQl9Yam&W$u>t7?d z%Z7U2kRrOXbILgHKcq)=c3IgZ?*VDQ&ZeeZ=Y8K;Z24xzd(dFvq(68M8*Gi`Uhffu zJ!f6*{lZ|IZ4Z0DBBt;bvBkc{d)#oo;dsvbt-+4M9`E-C3rKH!PazZV2;L|D=ZN>e z|C2-#6lKw8X1A8o2Qts#E|mmJGkrb_G!)YO(w~;OHPYuZvp1pNS_(2>|MgIrOKHXa z-)seY-?p;<;gGiW5_ml5GsLBt^ywgtN1Gm>o?rP7XYto6wg#yBZT_D}YXH~3fYJGc zBXC9q{0Z;lB{&yvlFzgS*t2{-3HwCvYE>swB=ygPI>n!>Kb+XTA;@)Vox`U3giYsBeelW4lu4Gmj6rd*Fp zLjng*f!2Hc5YEidIX>p5u!B-V{ZIh*4*>y+B0r@py>QbU12MXQpQXA({y zEm(`U?KEkM(mE7TeA1VoE4I!A&){iehOLFPuhGg#oX0;GQiu;Jn3rzN zcRHHapYzfbB=N%3wlb@}V=8ARqSdY~6ASk{5qf6i*(#$ofgdy>T7G3?-l_az5Y1B( z&eN|aI$KlD)|qUc8e{$AxBv9(9u-Io5zV6QtUb_DpRHnT3}_N9d^3_5MJ?Vy1!s{G zCHAA!?p1VM@QL?-ahb;T-}V}R*6Z|dz8q7(6Bl2tm)iCI=PuJ2shoD5ZXJpLpKX}; z|HO{{pKZf*Led@ZFK)xQ&S;Z1wqb?uR*hZS*lua)BtFYce04}{!-c(v<-c6MkNA+oi5`?lW11DX~| zH(oTk+V0XCuazZKv9LXyc*9C%iN@RO;vR@bm9)3#CTrt^Sop(_Pa~8%>QkvstLn`qz4P zrkj@1Z%cIc$Yi|y1fUu0Kwm}iBPQWjzsW8@bbwNY2OsogJfCqO<50#?j5Ul6jMEtB zF#Z8C4d3mjbjuhYMa+h$*xrj+0PiFE;1k4l@FnXf7+=B$hVKPeuwB76#RzO@e%@Q+Iq+t>rI*CgmWdZ{vMS>&BP18p zSY}$BFoo@DZ2y7nMV6_k-^=zgwi_KIBquCmT*tVPaWCWBjE5MHGM-=*PAcVQOhYWe zukj>EpL@a`f5Zl8L#4d0*VsBW1 z*ca9#UINb{4#Q6bJHi-{{2if|@oMm)Jp-`|RyNh&7w*K&eZhfvAy;3EO4SyrmZe%y zVGzzB1EQq)dYWGG9lb*44&TyliHQO20* zVUYbXQwwxM+=`{*ti)M~vjS#{0W-ybnPOyCWL9KWLac;X39%ApCCo~g6h(@#6k#dC zK~YwstVCIfu@Yk?#!8%(I4f~hAc-54#0^T~2C*WuBC`@=CCnIMtd8;_#u#S=3nh{n zLpoB@kcEk-x?tVdXnvJzz_%1X?G^(xjl9jqbzDitmw z#u#Iq(d?sUg?#K~jQgl@aUV4cE zOA(eLtVCFevJzz_%F0@QG7e^p^%(0h*5j3?FidZwxevv*p9IsXFJX|mTy#7c;jFvTeTFiT;UA{-Q9 zCBn*rB077boG!|Gl=T=###o8566csWD{)p1a#<**ilLaBSj_9Wm^vdDQ@526lMs_I z+hMjNY)9B$P|S^BCCW;S?HJoT*c)dh&I*+9SeEeIur0IQuY~d}VC+{)c8!7uyxK4^ zx1*ukQBK&e8{t959o1y7?M_(SgLTHWJ;|=?MQHBLcn*6R7o1D>j*)~Hf@~d#ufg~l zim&1L8ilWM&|bFIpiz&n27FDy*ED=tg=|g3*Ct7}K7}tE-ge)hrj6^PEcH6@?nvMr zm7%lfpo;Sc?MF>GHBsy9x0@x59L$3nRyg?sfVz|pM^Ld74_t}Ny;~n zk8S-oq6FXKBZ9ymrxl?69o{tx_#RP$lTd>84~P={2<^~5g(zV|$`FNUk0^-_h$hhq zy=FxG97R-N-(Q6;Ffh9Vbc4Zg2MiKI42SRN!x`~A2*~(us1T#T=_O$dxbo>Z4qhpy z<5kc#myR`1QA)>JXvXhy3NaBj70|H(ztc^}tMSX+besY&aGq=7(mdkRfPT3u#0(h9 z{+UpOKkyXddXPD84xGrxcf*9Z5x<>H$D8qM*>qH3y@!tT;KfWjZpCl3(s7K~7^LHa zVvtP7X7LQ`4~siWh;I}n$#mQ#CgYQiz{ihA@n@()w205R+!oP+`4i$=K0YOOEBG5) ztUx65ahn*z$7jXee0)yem;2EFyx7MHcZ&Tk;x7ujn~pDuYCgUq4sZ|m2#Fi;H!+$= zXs@W^<38cw$=vBudVFfwW_wVx^wrc+6lEiyY*Bj-QP|0Qdi#J-tFq% zv9e=TWyj9d-K(lw_TN8Or0JtU)Ie9L#(ks< xRN;R;{!f4&$R^;EY8`s&&|e4RkyYYvdAni^S=G=P8QB!q6L+AV>Sb#l{V%;9bmjm6 delta 16281 zcmcIr3t$sf);@QV=AATY(>8sl>63@06iNXVQYb1a3JNIl3bY_qU@Z>?Uy}+{R1oV3 zDk=skyCARvl~qtMtB9~Gz7X-n5*0*IR9032AN=Q>nM}*W)nES~oo~+f-E+^q_uM;o zW|B1gsubQX-8VP&@ax~r1?}HA5b)cjGJqvbOBN>#d>){?QnmCvWu{289e5O-{CF%; z7K;udqHGg=OgHodh&8mhq+O=$HvmkjFl4;Eo zyOi^-KIJEixn+d)Ka$w3-0Ub8?=bFB#-|mEca`mq3h|!uv!hbH&-j5d+1X8es4RDO z7JFOvI7b@9zmy73zSyS>^W=$`@;KTbF@CJPvlqo({1|;mWX3 z{G11PNSTMq7mQyr9#-7GcH%4Yv473IcBaPc?Uip*v&A=D^N4cDw@Y}HE$PJ~Md_89 z<~0Z~*^`8+#0b!#eUW4|l%y)244+6)+GmtjrC~A6MRJ)0NqA1Dxk!Q%^C{H=lA*-M zo~Qd+m4`FRlmqELVOM@m?;xCtE3=cxq^6aRGLl6WwWT;SUCM&YY>}<3$;=ly%A1)T zMXvI5W})ya1z8iUt^!&Ni0Qt8A9L4k7?a;n*+LXG< z*G|Q*!uH7kwRp}SawT;96Y%8aUpS;cy25$Lo0W0dT~Z1Kz-JNw{txHkaTW4S%9`v> zqOYNFphqg@qXEIb{+KJ)>VPQ6oXAs&( zW>@1F2Y~?+Kw#b7d54R~V2O!YZ!fV#jRmubrD|*jduC~@!b4?c8uJtqt3=j-G3EJz z!^PuZy;k@#S6mNO+{8D`Hfn4Vvv)Q2BeQ)PJIL${WL6wR2W+qbzGnvT7_;Lr7#nm$ zEQ8uR38T1$XJoEu5aXCW!pyC)mCSs^FnwVRGY;Azvm#yZq9)plf5R*cT7moMIOrmZ zgdN%aE`SE;imV9c=VhCVpv9MmI3T;gY%+c4EH)1fmwDSGW;ta#HKY@s<0S0N_ya59 z04aT0xtMVnB84bGy{4cP@s&cxEW)Lkg!76CpD8M3&vLZ)Wmh16oJ07egK!UHsEm3w zflFtyzJ-<1PU^}6RvH+;ZarRGiP|PwOKpxwJ*`CC$)G~g^o+GlVWOV(8rQJLZJ5c4( ztk@X^D?`}+YZ{fNvTb7h8n*Y@NWYNnK8$xV&S3=Z;*D(I!Wd>eg3)mOZ{(nNVv^&V z!ya2d3|9(-1&kStH)fDhh8XwkR@5JrSDb|JGk(T6CWn-rY{wYCQte#Q(_Dn9endH3 zoX8}b@cuNyqg;6iD}@=PJY7uqXVwpM<?`dWXuqay>d_ISEU%31{RG z4sZ|-V*MpI+2H`88wgA6gab+l3-jvRgqx1aEGJ>Pi*Spb@M*?-a>%Z7OmE{qlh*$L z$4o@0pKy?q(2zq|kw>`2PWUwAJ*-!;|53JwWK-#1iwPsF9CVT0go=P=uG>z@lYnnH zH=}%%x#59gGd};!%EOxhRNBk6G@76<-mg+&a&7}E5Be#+wOGLf8(fWuk9qD!T;dvO zzQ|9{HI2T5x%6x^P-hD<_ynCTMs}0VsH{Gr$Io9n5Yv8)37?w&paO6X0cy9YaqO?9|wXjApYLc5AGsquDH5V6R3S zoXut{9MD)lbhW`@jhQo>&35>Y#;RS-W(WMFv3oO~G&{kR2l#Ihtj>MX?1Ds%b#bgU zCqkOW9?V&5c0;bl#uThIdt@lq=;o}o<|OD8XN7Cc$xx}W-ri@-UI=QeI(>^d1t#ij zr#TZA#Xa*rH2YyGvnD9Q90p*u#^gA9$0!g}5D`4b(( z(Byw4;}bK!CTnbC?k8q?Sy9VgaIH0;r?WMf#PfAF5tG+$|a~Raw#hAk? zDAAgqkp8*350+v3)cnA_W9ExASH=H~@uc};ot;QHX}&~fdFGSmOLdlGIcXlCvkdD= z^JO~gZ#!weTxZY2N%KIREfXirgNVu4yy9Okoitye7rtgVX}(ft|H94;)>)D1qPF{snN0|ZHgEH*Qksy$`Mebvji9cQ<*KYBjFBYIDVoTmQirGR#@uu zT1Lae8k^?zT0*d#StIPiD}4;C*O}ik2BI2!E4#!p7IrfGRCEuNB8xHG2HgW)Ej5sk zPd(oTxt=Ma7M#RnY`(c&4@)iNa^WJ&T4dv)tHw4Jdo2?ns8#f*&w@G_$*j@xS2wYn znW?)+9WN}nR?;6aTUiiky+$&HplT)0vzY{zW9#wG=-EOoF+W02hs*=s6; zZobQqz00f#)+Z0OT(3{ebuh+)ujd6MEV9gPH_1{DcKw~-=vl~Nc(>`aF zEO-XlYK^UQJ&$ao#y(7Y+0p>pn5h%f0B@)a4(IN`vM)6@TK4X++yRzi9ussZe8Vye zN;J03^PZ&8y`60?%q}sN-Vm6L3st*I1W>4?mHiphF$kT30}|&Zbyb zL9NE>oi|(8KtyMAtxrKzXNqiH2Vd&+QELnEXM@lP8=RL}o`$aU=@f3I7qolA`ZpM% zv2e*c>vJ$$V=tCOtx;I5F=M;etuMpd8k>~-p>+p1I;c@vk`7sSLbb+@BptKLZ^Eq_ z?UZb??S`i{wkkQv_AY$HY!NQRT-#narn8Q=7^HQipo`!ZM>pHY&|PQ!Y@fk!jjeSI zvK@x7&c@h|!g7u6#j+DJY}e@+n-GUJc87bA%`DvKs7>7A9%FNf3XRqICfmGXpw4c! z`NRzxD=)dzmMK=~>|tB3*sifV+$(JP0!rgCH`_j}qNAwhGx;K_y|larG&ww2v2qxvUWud3)LG z#5|o{WWQdl(%3@Z<@SlHdq0ZF_O8eX6ML9B1|Ro5VPceU*HR{buo~ z&Su$f5t}sjY+x?3qdHUUw+nB%8pY_GYY&ToI#cYk#0?tT>v_aJN31TV`wuq1*R$L{ zSG=kf4sflu-z|(?)Gz~Ff3wdM`5K#3u-QIegmm@~dy{C;SaZSK_6NkXI{TM>k=UcL zvYZ3<2Zgt*8nZ0t-}XmDcVyUpBRp!q#lA$$iyCFxOiG)kNWJ6W)aia8^u|U zRl-q0VVc0;ecifVbk*4QK&j(zBBZfzU8Roa#XOCj@bq%LD4x~WRM!BjR-Qw5V9Z;P%PtID~}@t!u@iod|Nz!B5g1F*pHk1>Bs>-bz_vf|$(z2NvtF9hRjj&F6=BVn)On9eRU9di7rv;OAe4*pmf7oy_t zXR$bq5)FY^A8VS^qO<9?0;gSPIilR@jx*WcN$TxPj#IzGaG5h*XJd_*IsH0YnsAx3 zSZC`^qnxEWd&)e~S+28{mYL2y2-a3;P7$0y}>-5cp<<3iV_L*s~ z;|iU9X@15zRA&b)&p1cu>|^UI&apbHgm;`1bk<)Sa8AjVwUuT z#s>NlV3xF6W4#0N6fs-cpwR(=1eh&FHP$b;$}&gVrLm-(myvy;v7ep9j%loq$LE?O zNtHDFjh2C)Y-A}K^JBe6$*(f_t~lS-D3vi=1e=}hW!GKO)jI9znkU_)vkP7KO0#u# zg{w(w*4RwvH7-SZRc8}i3#CIko9bF59o5;bu7@NqzG={{YPJ0i*Td3zI=kN`KPnB^ z>Eo^?(gdA7=~^ny)!4O;^{!>oTAjV<`m^-9&R%z|kUrGe`>s{e56skOpEZ)P2Q`oT zMajpmHIiRr6WV?0S|>}@8l9hf%(X$9rm>=ShQw#3hctG1@>JJz(sqqKQQ}P8EPbi5 zT}kPQf0wfO<*5nwmlPy!m4X`cB$p*_*B4ub@!Z6J>TGgCzr@!i-v8-}Z7>Z=+@%-Z zY95*Rj?QLSu1$PTXVb0qi67|f&$inV_v!2lxGV8vo&78pCVq-cKtFumJ1fe+RPcYO zORzN^zix$$BKlrsG;S{m&?DXLHi0d` zQu*vD{ngC14ifJ_ln~&A(6@Z2jreyqaMqc|ZnbS#YgfjZXl-~!8l`uX=j^DK{*XhZ z-(xKSW%xKQ!CUzJe5NhnNt|N|heRJv?ZKF2(>i#P>#B;}R#F>?quWg(DD?ZuRJjft zGr;4lsOxz=c1417GfCf!O&Pfv0jKKfN})K-E)DfbZ{cw3*=OsLJbEp4;@e{YDk69mgiV;U1TC?@r&K;$WeRH4g3=0zFPuNFQv`z+IVTSz%dpfhP*OwF zm=bXQXvJ=FwVfwbY0X3xzOYNM8#`x!(U=b-j0n)aMk^zJ8K2F^?M~p#;kPOrL5^>Y z6oLZb$3z&&#DUDGxSq}P^v2ry9Kp{0YBbvajSzSJZ{aHZzabn)i+ifkD09<1t+{-{ zp>FM}E>lWLQWunfoV+t#TUU5a-hcC_=~MqvoYu)|8}l?DnRlr8`fuGJX)~FRt34i0 z-5XldOVzB?l~2Ol^uj@^m$AUQt%|<>TkrRjWC^pPu8CIHeymJ%w|bHy0V%Y4R8JL2A0JS4 zx{r`e&7V;!W5Mag)Vz9A+3%)#C0m017=Xs12BKQ=a8z1V7;nso9im#KP?Mxj)at69 zYN;9x_kUV4t(9vqX>|W?tz3f_k9q^8RZad&{iknPG(l%8rCcWP3R5>FbKAX-;>b8d z>fT3dQk`!TkKhbf-Sd|kqcxF$sisBuTYcoS^_mi%fC>NgK)=+?Zyt=y_x|`z=$Fz< zlY&1y!VJZ;qBh)$|MeLHr$+TW^A0If^5e~)S^WD-)d^Lb{FVEsjI$MA0l%dJg+Ap5 zO1p&~L0tjrLmNf=_5BgoKi#7!y=}LY)(tYg>-|6NkMbECB=wq}O|=d3ly0dz9gX;p z?T_(whR^@;#acdlY{*SN! zGspegN27ZFQvTE-op~9rkok=#{>nj{=$ZEEuM>1N(?iN8eCCfoww(F!-nKOUCPeY$ zbyXYJfBc1kF0J?*Pu%H`H1M11()a)I-q90Tdw!#%5x02;EX5G*vA~;H zCcIFcR4I0FS2kh=H+1C23gBCJohX2R;D@dq+3v_T`3UT2H2YogcUbFlN;||kEjuoG z$}EpF4V4^FXS%^;hkCZBvwektd^r|S#M z#V`vtNX5_|9!hEt8F1WFiho+LK`Mo089qoXg-qCwm=6YbDRck_Vi&j=@mv^&*cZkl zUIMovUI}+24u>s>HH_E8t7uO}?1GK8_w(LztrtQkZd; z6o4|%EGkvT4Cy47c?<^f^c%=0-$14LtOQxMl5NXo>kN@Of{a{8S# zrD3-HE^gXIPJS0P7i1;KN|2Rm7cJ!w>mk-dtk<$0Wv^% z9K>HE;d*0?GR7EE@9`EQMGKvlM0}%u0ln2rCg*nmtLF%qZ(o)}yS)Sc$O`V`V>k8k5PtnlYSA!-z0O z8Dlb^z)J;w#vo&evDQnK!(JL+*h}LJdue#7dZzFe_nJBCKppp-%5-jQFUrh>seJ_$YFe zl_)Dw-*YlbF_vO1#n=hbxsU1G$8_!@D}Gk|tOQvJvJzw^#7c;j5G!F;!mNZ@iLer3 zC4!2KJz^=!Qk0!yti)J}u|hwOTVRF^y4?J1``He%9b`Mic8KjT+hMjNY)9CRvK?hR zMmDa0CXmSuWO4&+``He%9b~&YlXqQKLac<@4znF$JHmD|dq!D_vJzuE#x`VeTUpeW zY|NsdewO?!1=$X=9b!Agc9`uj+Yz=SY)9FSvK?bP#x`VgGub>Ow*71e*$%QDl9_~< zgxL&&6#g zj|&)Mj1XWYz!zSCFT4OP!`T7KR)qBk>k-zYsC4s0S&6a|W1ko+F;*a-d?23}bv{Ki z=D$Si-;V{j({q8J3xez#WF^Q-h@|ps+x-o(*}n;~ zK0wMB1zQm}xOO1M(%wRB8|M&3Xa|FR6m(F4uzxOvOU)ry$wfHW^A2JGD$3%~6XbtG zF81|XL{ABT8@riqZZ7QGy?_F$sQx4v0U)Ify5r z6PAMLgf>2w@?%068|?q+J|4zp04%spQ$~IUvdDx8+q|%h)sM4dD(eC{xZ5h9D@7d zHgP}rrJL|jY&91u_l?U_rqvu#&Y5gcZXNGd7S{ft*y=Krb>m)A_D;xD8m>)M240ub zGH<+DCyF|+mk31%oRMw6uE9=^&yr!mex0-R4W6H`qkF6bF z+qt5=va)-R?qe&ukMCAKc6@~rT~Z;sE8i{YV7FD4mX-D>?cAetSxf2Tw~H)*xvxC8 zg3=@5F%plt&*QO#l%^9IN-Sb5d9keH!@dclE?j?d{y#R3A3h1MVz{X1h=Ei7bltd` zX;VjBaKXTd)32?Y+~Qa&31#5Yt6N-8{9C%b68}t@{{FNKy5faij<1w8_yI#V#Bum{ qs8mvpd@Op6$D=cD1Lde!LU;TCqC4sph~v?}9PRN?p Date: Wed, 12 Mar 2025 15:18:20 +0100 Subject: [PATCH 622/865] Simplify passing of the device (suggested by @rootdarkarchon) --- Penumbra/Import/Textures/TextureManager.cs | 32 ++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 6adf5861..8fab097e 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -50,11 +50,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, string output) - => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, input, output)); + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, input, output)); public Task SaveAs(CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) - => Enqueue(new SaveAsAction(this, type, uiBuilder.Device, mipMaps, asTex, image, path, rgba, width, height)); + => Enqueue(new SaveAsAction(this, type, mipMaps, asTex, image, path, rgba, width, height)); private Task Enqueue(IAction action) { @@ -159,30 +159,27 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur private readonly string _outputPath; private readonly ImageInputData _input; private readonly CombinedTexture.TextureSaveType _type; - private readonly Device? _device; private readonly bool _mipMaps; private readonly bool _asTex; - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, - string input, string output) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, string input, + string output) { _textures = textures; _input = new ImageInputData(input); _outputPath = output; _type = type; - _device = device; _mipMaps = mipMaps; _asTex = asTex; } - public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, Device? device, bool mipMaps, bool asTex, - BaseImage image, string path, byte[]? rgba = null, int width = 0, int height = 0) + public SaveAsAction(TextureManager textures, CombinedTexture.TextureSaveType type, bool mipMaps, bool asTex, BaseImage image, + string path, byte[]? rgba = null, int width = 0, int height = 0) { _textures = textures; _input = new ImageInputData(image, rgba, width, height); _outputPath = path; _type = type; - _device = device; _mipMaps = mipMaps; _asTex = asTex; } @@ -207,8 +204,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => ConvertToCompressedDds(image, _mipMaps, false, _device, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => ConvertToCompressedDds(image, _mipMaps, true, _device, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -326,8 +323,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public static BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel, - byte[]? rgba = null, int width = 0, int height = 0) + public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, + int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) { @@ -337,12 +334,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, device, cancel); + return CreateCompressed(dds, mipMaps, bc7, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, device, cancel); + return CreateCompressed(scratch, mipMaps, bc7, cancel); } default: return new BaseImage(); } @@ -390,7 +387,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public static ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, Device? device, CancellationToken cancel) + public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) { var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) @@ -405,8 +402,9 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur input = AddMipMaps(input, mipMaps); cancel.ThrowIfCancellationRequested(); // See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition. - if (device is not null && format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) + if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB) { + var device = uiBuilder.Device; var dxgiDevice = device.QueryInterface(); using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); From 442ae960cf429ddf9703252615916acf3fa0d679 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 20:03:53 +0100 Subject: [PATCH 623/865] Add encoding support for BC1, BC4 and BC5 --- Penumbra/Import/Textures/CombinedTexture.cs | 3 +++ Penumbra/Import/Textures/TextureManager.cs | 16 +++++++++------- .../UI/AdvancedWindow/ModEditWindow.Textures.cs | 12 +++++++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Penumbra/Import/Textures/CombinedTexture.cs b/Penumbra/Import/Textures/CombinedTexture.cs index c1a22088..f5f921be 100644 --- a/Penumbra/Import/Textures/CombinedTexture.cs +++ b/Penumbra/Import/Textures/CombinedTexture.cs @@ -6,7 +6,10 @@ public partial class CombinedTexture : IDisposable { AsIs, Bitmap, + BC1, BC3, + BC4, + BC5, BC7, } diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 8fab097e..0c85f5be 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -204,8 +204,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, false, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, true, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height), _ => throw new Exception("Wrong save type."), }; @@ -323,7 +326,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Convert an existing image to a block compressed .dds. Does not create a deep copy of an existing dds of the correct format and just returns the existing one. - public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, bool bc7, CancellationToken cancel, byte[]? rgba = null, + public BaseImage ConvertToCompressedDds(BaseImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel, byte[]? rgba = null, int width = 0, int height = 0) { switch (input.Type.ReduceToBehaviour()) @@ -334,12 +337,12 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur cancel.ThrowIfCancellationRequested(); var dds = ConvertToDds(rgba, width, height).AsDds!; cancel.ThrowIfCancellationRequested(); - return CreateCompressed(dds, mipMaps, bc7, cancel); + return CreateCompressed(dds, mipMaps, format, cancel); } case TextureType.Dds: { var scratch = input.AsDds!; - return CreateCompressed(scratch, mipMaps, bc7, cancel); + return CreateCompressed(scratch, mipMaps, format, cancel); } default: return new BaseImage(); } @@ -387,9 +390,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, bool bc7, CancellationToken cancel) + public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) { - var format = bc7 ? DXGIFormat.BC7UNorm : DXGIFormat.BC3UNorm; if (input.Meta.Format == format) return input; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index d0764808..274c216b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -26,9 +26,15 @@ public partial class ModEditWindow ("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."), ("RGBA (Uncompressed)", "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."), - ("BC3 (Simple Compression)", - "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality."), - ("BC7 (Complex Compression)", + ("BC1 (Simple Compression for Opaque RGB)", + "Save the current texture compressed via BC1/DXT1 compression. This offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha."), + ("BC3 (Simple Compression for RGBA)", + "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA."), + ("BC4 (Simple Compression for Opaque Grayscale)", + "Save the current texture compressed via BC4 compression. This offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha."), + ("BC5 (Simple Compression for Opaque RG)", + "Save the current texture compressed via BC5 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha."), + ("BC7 (Complex Compression for RGBA)", "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."), }; From 4093228e6150c87cc3968ff0371f6a8f64cfcf36 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Wed, 12 Mar 2025 23:04:57 +0100 Subject: [PATCH 624/865] Improve wording of block compressions (suggested by @Theo-Asterio) --- Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 274c216b..4664372e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -25,17 +25,17 @@ public partial class ModEditWindow { ("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."), ("RGBA (Uncompressed)", - "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."), + "Save the current texture as an uncompressed BGRA bitmap.\nThis requires the most space but technically offers the best quality."), ("BC1 (Simple Compression for Opaque RGB)", - "Save the current texture compressed via BC1/DXT1 compression. This offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha."), + "Save the current texture compressed via BC1/DXT1 compression.\nThis offers a 8:1 compression ratio and is quick with acceptable quality, but only supports RGB, without Alpha.\n\nCan be used for diffuse maps and equipment textures to save extra space."), ("BC3 (Simple Compression for RGBA)", - "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA."), + "Save the current texture compressed via BC3/DXT5 compression.\nThis offers a 4:1 compression ratio and is quick with acceptable quality, and fully supports RGBA.\n\nGeneric format that can be used for most textures."), ("BC4 (Simple Compression for Opaque Grayscale)", - "Save the current texture compressed via BC4 compression. This offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha."), + "Save the current texture compressed via BC4 compression.\nThis offers a 8:1 compression ratio and has almost indistinguishable quality, but only supports Grayscale, without Alpha.\n\nCan be used for face paints and legacy marks."), ("BC5 (Simple Compression for Opaque RG)", - "Save the current texture compressed via BC5 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha."), + "Save the current texture compressed via BC5 compression.\nThis offers a 4:1 compression ratio and has almost indistinguishable quality, but only supports RG, without B or Alpha.\n\nRecommended for index maps, unrecommended for normal maps."), ("BC7 (Complex Compression for RGBA)", - "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."), + "Save the current texture compressed via BC7 compression.\nThis offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while.\n\nGeneric format that can be used for most textures."), }; private void DrawInputChild(string label, Texture tex, Vector2 size, Vector2 imageSize) From cda6a4c4202866deb8d26fff7726d4c2e99f450a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Mar 2025 00:13:01 +0100 Subject: [PATCH 625/865] Make preferred changed item star more noticeable, and make the color configurable. --- Penumbra/UI/Classes/Colors.cs | 2 ++ Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 4c0d1694..9c15ceb8 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -34,6 +34,7 @@ public enum ColorId : short PredefinedTagAdd, PredefinedTagRemove, TemporaryModSettingsTint, + ChangedItemPreferenceStar, NoTint, } @@ -110,6 +111,7 @@ public static class Colors ColorId.TemporaryModSettingsTint => ( 0x30FF0000, "Mod with Temporary Settings", "A mod that has temporary settings. This color is used as a tint for the regular state colors." ), ColorId.NewModTint => ( 0x8000FF00, "New Mod Tint", "A mod that was newly imported or created during this session and has not been enabled yet. This color is used as a tint for the regular state colors."), ColorId.NoTint => ( 0x00000000, "No Tint", "The default tint for all mods."), + ColorId.ChangedItemPreferenceStar => ( 0x30FFFFFF, "Preferred Changed Item Star", "The color of the star button in the mod panel's changed items tab to prioritize specific items."), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index 700f1d66..f70d63d2 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -12,6 +12,7 @@ using Penumbra.GameData.Structs; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.String; +using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; @@ -213,6 +214,7 @@ public class ModPanelChangedItemsTab( private ImGuiStoragePtr _stateStorage; private Vector2 _buttonSize; + private uint _starColor; public void DrawContent() { @@ -236,6 +238,7 @@ public class ModPanelChangedItemsTab( if (!table) return; + _starColor = ColorId.ChangedItemPreferenceStar.Value(); if (cache.AnyExpandable) { ImUtf8.TableSetupColumn("##exp"u8, ImGuiTableColumnFlags.WidthFixed, _buttonSize.Y); @@ -286,7 +289,7 @@ public class ModPanelChangedItemsTab( private void DrawPreferredButton(IdentifiedItem item, int idx) { if (ImUtf8.IconButton(FontAwesomeIcon.Star, "Prefer displaying this item instead of the current primary item.\n\nRight-click for more options."u8, _buttonSize, - false, ImGui.GetColorU32(ImGuiCol.TextDisabled, 0.1f))) + false, _starColor)) dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, false, true); using var context = ImUtf8.PopupContextItem("StarContext"u8, ImGuiPopupFlags.MouseButtonRight); if (!context) From 87f44d7a880203ee40a03c4a7e660e68ac6444e9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 14 Mar 2025 00:13:57 +0100 Subject: [PATCH 626/865] Some minor parser fixes thanks to Anna. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 96163f79..c59dd2e6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 96163f79e13c7d52cc36cdd82ab4e823763f4f31 +Subproject commit c59dd2e6724be71dfe6ade11dacf405f29634fde From dc47a08988d81844738051072a9e96607f5da9c7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 13 Mar 2025 23:20:54 +0000 Subject: [PATCH 627/865] [CI] Updating repo.json for testing_1.3.5.1 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 8de11f0e..1617a879 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.5.0", - "TestingAssemblyVersion": "1.3.5.0", + "TestingAssemblyVersion": "1.3.5.1", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.5.1/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 0213096c58e0f3a232f1b1602b88970c37c8a624 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:45:42 +0100 Subject: [PATCH 628/865] Add BodyHideGloveCuffs name to eqp entries. --- Penumbra.GameData | 2 +- Penumbra/Collections/Cache/EqpCache.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index c59dd2e6..1c1b3d1b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit c59dd2e6724be71dfe6ade11dacf405f29634fde +Subproject commit 1c1b3d1b2f050ae0424cb299d30b2bbb65514aa6 diff --git a/Penumbra/Collections/Cache/EqpCache.cs b/Penumbra/Collections/Cache/EqpCache.cs index c681b230..da1a1d44 100644 --- a/Penumbra/Collections/Cache/EqpCache.cs +++ b/Penumbra/Collections/Cache/EqpCache.cs @@ -48,8 +48,8 @@ public sealed class EqpCache(MetaFileManager manager, ModCollection collection) ? entry.HasFlag(EqpEntry.BodyHideGlovesL) : entry.HasFlag(EqpEntry.BodyHideGlovesM); return testFlag - ? (entry | EqpEntry._4) & ~EqpEntry.BodyHideGlovesS - : entry & ~(EqpEntry._4 | EqpEntry.BodyHideGlovesS); + ? (entry | EqpEntry.BodyHideGloveCuffs) & ~EqpEntry.BodyHideGlovesS + : entry & ~(EqpEntry.BodyHideGloveCuffs | EqpEntry.BodyHideGlovesS); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] From 61d70f7b4e994de1674bdfdcec883c54524b7dcd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:45:56 +0100 Subject: [PATCH 629/865] Fix identification of EST changes. --- Penumbra/Meta/Manipulations/Est.cs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 8a450eee..05d4c014 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -29,19 +29,24 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende switch (Slot) { case EstType.Hair: + { + var (gender, race) = GenderRace.Split(); + var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", () => new IdentifiedName()); + $"Customization: {gender.ToName()} {race.ToName()} Hair {SetId}", () => IdentifiedCustomization.Hair(race, gender, id)); break; + } case EstType.Face: + { + var (gender, race) = GenderRace.Split(); + var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", () => new IdentifiedName()); - break; - case EstType.Body: - identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); - break; - case EstType.Head: - identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Head)); + $"Customization: {gender.ToName()} {race.ToName()} Face {SetId}", + () => IdentifiedCustomization.Face(race, gender, id)); break; + } + case EstType.Body: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); break; + case EstType.Head: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Head)); break; } } From 26a6cc473502952c704465432e97f7a9684a6a06 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:46:16 +0100 Subject: [PATCH 630/865] Fix clipping in changed items panel without grouping. --- Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index f70d63d2..b12df97d 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -247,7 +247,7 @@ public class ModPanelChangedItemsTab( } else { - ImGuiClip.ClippedDraw(cache.Data, DrawContainer, ImGui.GetFrameHeightWithSpacing()); + ImGuiClip.ClippedDraw(cache.Data, DrawContainer, _buttonSize.Y); } } From 82a1271281509e1158f4c5d3e421f603e0333fc6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 15:46:44 +0100 Subject: [PATCH 631/865] Add option to import atch files from the mod itself via context. --- Penumbra/Mods/Editor/ModFileCollection.cs | 16 +++++++--- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 30 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 20423493..7667910f 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -13,6 +13,7 @@ public class ModFileCollection : IDisposable, IService private readonly List _tex = []; private readonly List _shpk = []; private readonly List _pbd = []; + private readonly List _atch = []; private readonly SortedSet _missing = []; private readonly HashSet _usedPaths = []; @@ -41,21 +42,24 @@ public class ModFileCollection : IDisposable, IService public IReadOnlyList Pbd => Ready ? _pbd : []; + public IReadOnlyList Atch + => Ready ? _atch : []; + public bool Ready { get; private set; } = true; public void UpdateAll(Mod mod, IModDataContainer option) { - UpdateFiles(mod, new CancellationToken()); - UpdatePaths(mod, option, false, new CancellationToken()); + UpdateFiles(mod, CancellationToken.None); + UpdatePaths(mod, option, false, CancellationToken.None); } public void UpdatePaths(Mod mod, IModDataContainer option) - => UpdatePaths(mod, option, true, new CancellationToken()); + => UpdatePaths(mod, option, true, CancellationToken.None); public void Clear() { ClearFiles(); - ClearPaths(false, new CancellationToken()); + ClearPaths(false, CancellationToken.None); } public void Dispose() @@ -135,6 +139,9 @@ public class ModFileCollection : IDisposable, IService case ".pbd": _pbd.Add(registry); break; + case ".atch": + _atch.Add(registry); + break; } } } @@ -147,6 +154,7 @@ public class ModFileCollection : IDisposable, IService _tex.Clear(); _shpk.Clear(); _pbd.Clear(); + _atch.Clear(); } private void ClearPaths(bool clearRegistries, CancellationToken tok) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index c9a1d059..68424ae9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -75,12 +75,34 @@ public partial class ModEditWindow ImUtf8.Text($"Dragging .atch for {gr.ToName()}..."); return true; }); - ImUtf8.ButtonEx("Import .atch"u8, - _dragDropManager.IsDragging ? ""u8 : "Drag a .atch file containinig its race code in the path here to import its values."u8, - default, - !_dragDropManager.IsDragging); + var hasAtch = _editor.Files.Atch.Count > 0; + if (ImUtf8.ButtonEx("Import .atch"u8, + _dragDropManager.IsDragging + ? ""u8 + : hasAtch + ? "Drag a .atch file containing its race code in the path here to import its values.\n\nClick to select an .atch file from the mod."u8 + : "Drag a .atch file containing its race code in the path here to import its values."u8, default, + !_dragDropManager.IsDragging && !hasAtch) + && hasAtch) + ImUtf8.OpenPopup("##atchPopup"u8); if (_dragDropManager.CreateImGuiTarget("atchDrag", out var files, out _) && files.FirstOrDefault() is { } file) _metaDrawers.Atch.ImportFile(file); + + using var popup = ImUtf8.Popup("##atchPopup"u8); + if (!popup) + return; + + if (!hasAtch) + { + ImGui.CloseCurrentPopup(); + return; + } + + foreach (var atchFile in _editor.Files.Atch) + { + if (ImUtf8.Selectable(atchFile.RelPath.Path.Span) && atchFile.File.Exists) + _metaDrawers.Atch.ImportFile(atchFile.File.FullName); + } } private void DrawEditHeader(MetaManipulationType type) From 279a861582c7168604e6e995d801ff5add94c2fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 16 Mar 2025 22:17:37 +0100 Subject: [PATCH 632/865] Fix error in parser. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1c1b3d1b..757aaa39 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1c1b3d1b2f050ae0424cb299d30b2bbb65514aa6 +Subproject commit 757aaa39ac4aa988d0b8597ff088641a0f4f49fd From 03bb07a9c08f7fd867aa5344f62ce6070e97c1d0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 12:04:38 +0100 Subject: [PATCH 633/865] Update for SDK. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- OtterGui | 2 +- Penumbra.Api | 2 +- .../Buffers/AnimationInvocationBuffer.cs | 4 +- .../Buffers/CharacterBaseBuffer.cs | 4 +- .../Buffers/MemoryMappedBuffer.cs | 3 + .../Buffers/ModdedFileBuffer.cs | 4 +- Penumbra.CrashHandler/CrashData.cs | 2 + Penumbra.CrashHandler/GameEventLogReader.cs | 5 +- Penumbra.CrashHandler/GameEventLogWriter.cs | 3 +- .../Penumbra.CrashHandler.csproj | 20 ++---- Penumbra.CrashHandler/Program.cs | 4 +- Penumbra.CrashHandler/packages.lock.json | 13 ++++ Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra.sln | 52 +++++++-------- .../PostProcessing/ShaderReplacementFixer.cs | 12 ++-- .../LiveColorTablePreviewer.cs | 4 +- .../Interop/ResourceTree/ResolveContext.cs | 4 +- Penumbra/Interop/Structs/StructExtensions.cs | 5 +- Penumbra/Penumbra.csproj | 64 +------------------ Penumbra/Penumbra.json | 2 +- Penumbra/Services/MigrationManager.cs | 64 +++++++++++++++++++ Penumbra/UI/LaunchButton.cs | 9 +-- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 9 +-- Penumbra/packages.lock.json | 24 ++++--- 28 files changed, 178 insertions(+), 147 deletions(-) create mode 100644 Penumbra.CrashHandler/packages.lock.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1783c9a4..7901a653 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '8.x.x' + dotnet-version: '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4799cbed..c87c0244 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '8.x.x' + dotnet-version: '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 549c967a..2bece720 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '8.x.x' + dotnet-version: '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/OtterGui b/OtterGui index 13f1a90b..3396ee17 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 13f1a90b88d2b8572480748a209f957b70d6a46f +Subproject commit 3396ee176fa72ad2dfb2de3294f7125ebce4dae5 diff --git a/Penumbra.Api b/Penumbra.Api index 404c8aaa..6d262cd3 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 404c8aaa5115925006963baa118bf710c7953380 +Subproject commit 6d262cd3181d44c29891c9473f7c423300320f15 diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index 11dc52db..292be2ff 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Penumbra.CrashHandler.Buffers; diff --git a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs index a48fe846..89fea29d 100644 --- a/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/CharacterBaseBuffer.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Penumbra.CrashHandler.Buffers; diff --git a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs index a1b3de52..e2ffcebe 100644 --- a/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/MemoryMappedBuffer.cs @@ -1,5 +1,8 @@ +using System; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.IO.MemoryMappedFiles; +using System.Linq; using System.Numerics; using System.Text; diff --git a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs index ac507e7f..e4ee66d0 100644 --- a/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/ModdedFileBuffer.cs @@ -1,4 +1,6 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; namespace Penumbra.CrashHandler.Buffers; diff --git a/Penumbra.CrashHandler/CrashData.cs b/Penumbra.CrashHandler/CrashData.cs index dd75f46e..55460548 100644 --- a/Penumbra.CrashHandler/CrashData.cs +++ b/Penumbra.CrashHandler/CrashData.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Penumbra.CrashHandler.Buffers; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/GameEventLogReader.cs b/Penumbra.CrashHandler/GameEventLogReader.cs index 1813a671..8a7f53f8 100644 --- a/Penumbra.CrashHandler/GameEventLogReader.cs +++ b/Penumbra.CrashHandler/GameEventLogReader.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Nodes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; using Penumbra.CrashHandler.Buffers; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/GameEventLogWriter.cs b/Penumbra.CrashHandler/GameEventLogWriter.cs index e2c461f4..915c59a2 100644 --- a/Penumbra.CrashHandler/GameEventLogWriter.cs +++ b/Penumbra.CrashHandler/GameEventLogWriter.cs @@ -1,4 +1,5 @@ -using Penumbra.CrashHandler.Buffers; +using System; +using Penumbra.CrashHandler.Buffers; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index c9f97fde..4cb53c8b 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,20 +1,6 @@ - - + Exe - net8.0-windows - preview - enable - x64 - enable - true - false - - - - $(appdata)\XIVLauncher\addon\Hooks\dev\ - $(HOME)/.xlcore/dalamud/Hooks/dev/ - $(DALAMUD_HOME)/ @@ -25,4 +11,8 @@ embedded + + false + + diff --git a/Penumbra.CrashHandler/Program.cs b/Penumbra.CrashHandler/Program.cs index 3bc461f7..38c176a6 100644 --- a/Penumbra.CrashHandler/Program.cs +++ b/Penumbra.CrashHandler/Program.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; +using System.IO; using System.Text.Json; namespace Penumbra.CrashHandler; diff --git a/Penumbra.CrashHandler/packages.lock.json b/Penumbra.CrashHandler/packages.lock.json new file mode 100644 index 00000000..1d395083 --- /dev/null +++ b/Penumbra.CrashHandler/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "dependencies": { + "net9.0-windows7.0": { + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + } + } + } +} \ No newline at end of file diff --git a/Penumbra.GameData b/Penumbra.GameData index 757aaa39..ab3ee0ee 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 757aaa39ac4aa988d0b8597ff088641a0f4f49fd +Subproject commit ab3ee0ee814e170b59e0c13b023bbb8bc9314c74 diff --git a/Penumbra.String b/Penumbra.String index 4eb7c118..2896c056 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 4eb7c118cdac5873afb97cb04719602f061f03b7 +Subproject commit 2896c0561f60827f97408650d52a15c38f4d9d10 diff --git a/Penumbra.sln b/Penumbra.sln index e864fbee..0293df63 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -54,34 +54,34 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|Any CPU.Build.0 = Release|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|Any CPU.Build.0 = Release|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|Any CPU.Build.0 = Release|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|Any CPU.Build.0 = Release|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|Any CPU.Build.0 = Release|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64 + {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64 + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64 + {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.Build.0 = Debug|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.ActiveCfg = Release|x64 + {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Release|x64.Build.0 = Release|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.ActiveCfg = Debug|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Debug|x64.Build.0 = Debug|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.ActiveCfg = Release|x64 + {5549BAFD-6357-4B1A-800C-75AC36E5B76D}.Release|x64.Build.0 = Release|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.ActiveCfg = Debug|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Debug|x64.Build.0 = Debug|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.ActiveCfg = Release|x64 + {EE834491-A98F-4395-BE0D-6861AE5AD953}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs index 8e12662e..b9c21556 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/ShaderReplacementFixer.cs @@ -193,7 +193,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (shpk == null) return; - var shpkName = mtrl->ShpkNameSpan; + var shpkName = mtrl->ShpkName.AsSpan(); var shpkState = GetStateForHumanSetup(shpkName) ?? GetStateForHumanRender(shpkName) ?? GetStateForModelRendererRender(shpkName) @@ -217,7 +217,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic } private ModdedShaderPackageState? GetStateForHumanSetup(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForHumanSetup(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForHumanSetup(ReadOnlySpan shpkName) => CharacterStockingsShpkName.SequenceEqual(shpkName) ? _characterStockingsState : null; @@ -227,7 +227,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _characterStockingsState.MaterialCount; private ModdedShaderPackageState? GetStateForHumanRender(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForHumanRender(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForHumanRender(ReadOnlySpan shpkName) => SkinShpkName.SequenceEqual(shpkName) ? _skinState : null; @@ -237,7 +237,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic => _skinState.MaterialCount; private ModdedShaderPackageState? GetStateForModelRendererRender(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForModelRendererRender(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForModelRendererRender(ReadOnlySpan shpkName) { @@ -264,7 +264,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic + _hairMaskState.MaterialCount; private ModdedShaderPackageState? GetStateForModelRendererUnk(MaterialResourceHandle* mtrlResource) - => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkNameSpan); + => mtrlResource == null ? null : GetStateForModelRendererUnk(mtrlResource->ShpkName.AsSpan()); private ModdedShaderPackageState? GetStateForModelRendererUnk(ReadOnlySpan shpkName) { @@ -480,7 +480,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic if (material == null) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); - var shpkState = GetStateForColorTable(thisPtr->ShpkNameSpan); + var shpkState = GetStateForColorTable(thisPtr->ShpkName.AsSpan()); if (shpkState == null || shpkState.MaterialCount == 0) return _prepareColorTableHook.Original(thisPtr, stain0Id, stain1Id); diff --git a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs index c459a67a..0415fc9d 100644 --- a/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs +++ b/Penumbra/Interop/MaterialPreview/LiveColorTablePreviewer.cs @@ -84,7 +84,9 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase textureSize[1] = Height; using var texture = - new SafeTextureHandle(Device.Instance()->CreateTexture2D(textureSize, 1, 0x2460, 0x80000804, 7), false); + new SafeTextureHandle( + Device.Instance()->CreateTexture2D(textureSize, 1, TextureFormat.R16G16B16A16_FLOAT, + TextureFlags.TextureNoSwizzle | TextureFlags.Immutable | TextureFlags.Managed, 7), false); if (texture.IsInvalid) return; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index ea4506c7..41a27ed5 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -239,7 +239,7 @@ internal unsafe partial record ResolveContext( return cached; var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false); - var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName)); + var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value)); if (shpkNode is not null) { if (Global.WithUiData) @@ -253,7 +253,7 @@ internal unsafe partial record ResolveContext( var alreadyProcessedSamplerIds = new HashSet(); for (var i = 0; i < resource->TextureCount; i++) { - var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new CiByteString(resource->TexturePath(i)), + var texNode = CreateNodeFromTex(resource->Textures[i].TextureResourceHandle, new CiByteString(resource->TexturePath(i).Value), resource->Textures[i].IsDX11); if (texNode == null) continue; diff --git a/Penumbra/Interop/Structs/StructExtensions.cs b/Penumbra/Interop/Structs/StructExtensions.cs index 8b5974f0..03b4cf36 100644 --- a/Penumbra/Interop/Structs/StructExtensions.cs +++ b/Penumbra/Interop/Structs/StructExtensions.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.STD; +using InteropGenerator.Runtime; using Penumbra.String; namespace Penumbra.Interop.Structs; @@ -57,8 +58,8 @@ internal static class StructExtensions return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex)); } - private static unsafe CiByteString ToOwnedByteString(byte* str) - => str == null ? CiByteString.Empty : new CiByteString(str).Clone(); + private static unsafe CiByteString ToOwnedByteString(CStringPointer str) + => str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty; private static CiByteString ToOwnedByteString(ReadOnlySpan str) => str.Length == 0 ? CiByteString.Empty : CiByteString.FromSpanUnsafe(str, true).Clone(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 870865da..a09abcaa 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,25 +1,15 @@ - + - net8.0-windows - preview - x64 Penumbra absolute gangstas Penumbra - Copyright © 2022 + Copyright © 2025 9.0.0.1 9.0.0.1 bin\$(Configuration)\ - true - enable - true - false - false - true - $(MSBuildWarningsAsMessages);MSB3277 PROFILING; @@ -35,63 +25,13 @@ - - $(AppData)\XIVLauncher\addon\Hooks\dev\ - - - - $(DalamudLibPath)Dalamud.dll - False - - - $(DalamudLibPath)ImGui.NET.dll - False - - - $(DalamudLibPath)ImGuiScene.dll - False - - - $(DalamudLibPath)Lumina.dll - False - - - $(DalamudLibPath)Lumina.Excel.dll - False - - - $(DalamudLibPath)FFXIVClientStructs.dll - False - - - $(DalamudLibPath)Newtonsoft.Json.dll - False - - - $(DalamudLibPath)Iced.dll - False - - - $(DalamudLibPath)SharpDX.dll - False - - - $(DalamudLibPath)SharpDX.Direct3D11.dll - False - - - $(DalamudLibPath)SharpDX.DXGI.dll - False - lib\OtterTex.dll - - diff --git a/Penumbra/Penumbra.json b/Penumbra/Penumbra.json index 968bb750..924d7bd3 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 11, + "DalamudApiLevel": 12, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index aa2d445e..abc059e9 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -1,12 +1,76 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Services; +using Penumbra.Api.Enums; using Penumbra.GameData.Files; +using Penumbra.Mods; +using Penumbra.String.Classes; using SharpCompress.Common; using SharpCompress.Readers; namespace Penumbra.Services; +public class ModMigrator +{ + private class FileData(string path) + { + public readonly string Path = path; + public readonly List<(string GamePath, int Option)> GamePaths = []; + } + + private sealed class FileDataDict : Dictionary + { + public void Add(string path, string gamePath, int option) + { + if (!TryGetValue(path, out var data)) + { + data = new FileData(path); + data.GamePaths.Add((gamePath, option)); + Add(path, data); + } + else + { + data.GamePaths.Add((gamePath, option)); + } + } + } + + private readonly FileDataDict Textures = []; + private readonly FileDataDict Models = []; + private readonly FileDataDict Materials = []; + + public void Update(IEnumerable mods) + { + CollectFiles(mods); + } + + private void CollectFiles(IEnumerable mods) + { + var option = 0; + foreach (var mod in mods) + { + AddDict(mod.Default.Files, option++); + foreach (var container in mod.Groups.SelectMany(group => group.DataContainers)) + AddDict(container.Files, option++); + } + + return; + + void AddDict(Dictionary dict, int currentOption) + { + foreach (var (gamePath, file) in dict) + { + switch (ResourceTypeExtensions.FromExtension(gamePath.Extension().Span)) + { + case ResourceType.Tex: Textures.Add(file.FullName, gamePath.ToString(), currentOption); break; + case ResourceType.Mdl: Models.Add(file.FullName, gamePath.ToString(), currentOption); break; + case ResourceType.Mtrl: Materials.Add(file.FullName, gamePath.ToString(), currentOption); break; + } + } + } + } +} + public class MigrationManager(Configuration config) : IService { public enum TaskType : byte diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index cb533a00..49161c31 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin; using Dalamud.Plugin.Services; @@ -18,7 +19,6 @@ public class LaunchButton : IDisposable, IUiService private readonly string _fileName; private readonly ITextureProvider _textureProvider; - private IDalamudTextureWrap? _icon; private IReadOnlyTitleScreenMenuEntry? _entry; /// @@ -30,7 +30,6 @@ public class LaunchButton : IDisposable, IUiService _configWindow = ui; _textureProvider = textureProvider; _title = title; - _icon = null; _entry = null; _fileName = Path.Combine(pi.AssemblyLocation.DirectoryName!, "tsmLogo.png"); @@ -39,7 +38,6 @@ public class LaunchButton : IDisposable, IUiService public void Dispose() { - _icon?.Dispose(); if (_entry != null) _title.RemoveEntry(_entry); } @@ -52,9 +50,8 @@ public class LaunchButton : IDisposable, IUiService try { // TODO: update when API updated. - _icon = _textureProvider.GetFromFile(_fileName).RentAsync().Result; - if (_icon != null) - _entry = _title.AddEntry("Manage Penumbra", _icon, OnTriggered); + var icon = _textureProvider.GetFromFile(_fileName); + _entry = _title.AddEntry("Manage Penumbra", icon, OnTriggered); _uiBuilder.Draw -= CreateEntry; } diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index 4e6cf62c..bfe89768 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -164,13 +164,14 @@ public unsafe class GlobalVariablesDrawer( _schedulerFilterMapU8 = CiByteString.FromString(_schedulerFilterMap, out var t, MetaDataComputation.All, false) ? t : CiByteString.Empty; - ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->NumResources}"); + ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->Resources.LongCount}"); using var table = ImUtf8.Table("##SchedulerMapResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) return; - var map = (StdMap>*)&scheduler.Scheduler->Unknown; + // TODO Remove cast when it'll have the right type in CS. + var map = (StdMap>*)&scheduler.Scheduler->Resources; var total = 0; _shownResourcesMap = 0; foreach (var (key, resourcePtr) in *map) @@ -214,7 +215,7 @@ public unsafe class GlobalVariablesDrawer( _schedulerFilterListU8 = CiByteString.FromString(_schedulerFilterList, out var t, MetaDataComputation.All, false) ? t : CiByteString.Empty; - ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->NumResources}"); + ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->Resources.LongCount}"); using var table = ImUtf8.Table("##SchedulerListResources"u8, 10, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) @@ -223,7 +224,7 @@ public unsafe class GlobalVariablesDrawer( var resource = scheduler.Scheduler->Begin; var total = 0; _shownResourcesList = 0; - while (resource != null && total < (int)scheduler.Scheduler->NumResources) + while (resource != null && total < scheduler.Scheduler->Resources.Count) { if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) { diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 9aa1ebd5..dda6b305 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -1,7 +1,19 @@ { "version": 1, "dependencies": { - "net8.0-windows7.0": { + "net9.0-windows7.0": { + "DalamudPackager": { + "type": "Direct", + "requested": "[12.0.0, )", + "resolved": "12.0.0", + "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" + }, + "DotNet.ReproducibleBuilds": { + "type": "Direct", + "requested": "[1.2.25, )", + "resolved": "1.2.25", + "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + }, "EmbedIO": { "type": "Direct", "requested": "[3.5.2, )", @@ -52,12 +64,6 @@ "resolved": "3.1.7", "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" }, - "System.Formats.Asn1": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "OKWHCPYQr/+cIoO8EVjFn7yFyiT8Mnf1wif/5bYGsqxQV6PrwlX2HQ9brZNx57ViOvRe4ing1xgHCKl/5Ko8xg==" - }, "JetBrains.Annotations": { "type": "Transitive", "resolved": "2024.3.0", @@ -134,8 +140,8 @@ "type": "Project", "dependencies": { "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.6.0, )", - "Penumbra.String": "[1.0.5, )" + "Penumbra.Api": "[5.6.1, )", + "Penumbra.String": "[1.0.6, )" } }, "penumbra.string": { From 586bd9d0cc6e3ff308a69fa3ce6e65ff3fc41f2f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 12:07:45 +0100 Subject: [PATCH 634/865] Re-add wrong dependencies. --- Penumbra.sln | 8 ++++---- Penumbra/Penumbra.csproj | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Penumbra.sln b/Penumbra.sln index 0293df63..ac1c9566 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -58,16 +58,16 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|Any CPU - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.ActiveCfg = Debug|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Debug|x64.Build.0 = Debug|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.ActiveCfg = Release|x64 {EE551E87-FDB3-4612-B500-DC870C07C605}.Release|x64.Build.0 = Release|x64 - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|Any CPU - {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|Any CPU + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.ActiveCfg = Debug|x64 + {87750518-1A20-40B4-9FC1-22F906EFB290}.Debug|x64.Build.0 = Debug|x64 {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.ActiveCfg = Release|x64 {87750518-1A20-40B4-9FC1-22F906EFB290}.Release|x64.Build.0 = Release|x64 {1FE4D8DF-B56A-464F-B39E-CDC0ED4167D4}.Debug|x64.ActiveCfg = Debug|x64 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index a09abcaa..cc892fa8 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -26,6 +26,22 @@ + + $(DalamudLibPath)Iced.dll + False + + + $(DalamudLibPath)SharpDX.dll + False + + + $(DalamudLibPath)SharpDX.Direct3D11.dll + False + + + $(DalamudLibPath)SharpDX.DXGI.dll + False + lib\OtterTex.dll From b8b2127a5d63b3368ca047d206073a288b42166f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 15:53:59 +0100 Subject: [PATCH 635/865] Update STM and signatures. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/StainService.cs | 4 ++-- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs | 2 +- .../UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index 6d262cd3..2cbf4bac 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 6d262cd3181d44c29891c9473f7c423300320f15 +Subproject commit 2cbf4bace53a5749d3eab1ff03025a6e6bd9fc37 diff --git a/Penumbra.GameData b/Penumbra.GameData index ab3ee0ee..9442f1d6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ab3ee0ee814e170b59e0c13b023bbb8bc9314c74 +Subproject commit 9442f1d60578dae7598cbb0a1ff545d24905bdfd diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 0a437da0..b16d4dcd 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -16,7 +16,7 @@ namespace Penumbra.Services; public class StainService : IService { public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend((ushort)0), MouseWheelType.None, Penumbra.Log) + : FilterComboCache(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log) where TDyePack : unmanaged, IDyePack { // FIXME There might be a better way to handle that. @@ -31,7 +31,7 @@ public class StainService : IService return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3; } - protected override string ToString(ushort obj) + protected override string ToString(StmKeyType obj) => $"{obj,4}"; protected override void DrawFilter(int currentSelected, float width) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index f32a3dc9..0c987972 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -607,7 +607,7 @@ public partial class MtrlTab if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, scalarSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dye.Template = _stainService.GudTemplateCombo.CurrentSelection; + dye.Template = _stainService.GudTemplateCombo.CurrentSelection.UShort; ret = true; } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index f21d86a9..0ffdd1cc 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -184,7 +184,7 @@ public partial class MtrlTab if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; ret = true; } @@ -299,7 +299,7 @@ public partial class MtrlTab if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton)) { - dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection; + dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; ret = true; } From 124b54ab046888f49df8d28120d1d3990dc30c69 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 16:00:30 +0100 Subject: [PATCH 636/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9442f1d6..64823f2e 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9442f1d60578dae7598cbb0a1ff545d24905bdfd +Subproject commit 64823f2e29fdc65033d1891debe1ea18dadce1c8 From 525d1c6bf96ee2965e541d17a160a961f0ed85cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 27 Mar 2025 18:18:04 +0100 Subject: [PATCH 637/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 64823f2e..9ae4a971 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 64823f2e29fdc65033d1891debe1ea18dadce1c8 +Subproject commit 9ae4a97110fff005a54213815086ce950d4d8b2d From 49f077aca0eb850f3814af29d157911489e4967d Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 27 Mar 2025 22:32:07 +0100 Subject: [PATCH 638/865] Fixes for 7.2 (ResourceTree + ShPk 13.1) --- Penumbra.GameData | 2 +- .../ResourceTree/ResourceTreeFactory.cs | 54 +++++++++++-------- .../Interop/ResourceTree/TreeBuildCache.cs | 22 ++++---- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 9ae4a971..1158cf40 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 9ae4a97110fff005a54213815086ce950d4d8b2d +Subproject commit 1158cf404a16979d0b7e12f7bbcbbc651da16add diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs index cb8be184..49194c3a 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeFactory.cs @@ -23,24 +23,35 @@ public class ResourceTreeFactory( Configuration config, ActorManager actors, PathState pathState, + IFramework framework, ModManager modManager) : IService { private static readonly string ParentDirectoryPrefix = $"..{Path.DirectorySeparatorChar}"; private TreeBuildCache CreateTreeBuildCache() - => new(objects, gameData, actors); + => new(framework.IsInFrameworkUpdateThread ? objects : null, gameData, actors); + + private TreeBuildCache CreateTreeBuildCache(Flags flags) + => !framework.IsInFrameworkUpdateThread && flags.HasFlag(Flags.PopulateObjectTableData) + ? framework.RunOnFrameworkThread(CreateTreeBuildCache).Result + : CreateTreeBuildCache(); public IEnumerable GetLocalPlayerRelatedCharacters() - { - var cache = CreateTreeBuildCache(); - return cache.GetLocalPlayerRelatedCharacters(); - } + => framework.RunOnFrameworkThread(() => + { + var cache = CreateTreeBuildCache(); + return cache.GetLocalPlayerRelatedCharacters(); + }).Result; public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromObjectTable( Flags flags) { - var cache = CreateTreeBuildCache(); - var characters = (flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.GetLocalPlayerRelatedCharacters() : cache.GetCharacters(); + var (cache, characters) = framework.RunOnFrameworkThread(() => + { + var cache = CreateTreeBuildCache(); + var characters = ((flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.GetLocalPlayerRelatedCharacters() : cache.GetCharacters()).ToArray(); + return (cache, characters); + }).Result; foreach (var character in characters) { @@ -53,7 +64,7 @@ public class ResourceTreeFactory( public IEnumerable<(ICharacter Character, ResourceTree ResourceTree)> FromCharacters( IEnumerable characters, Flags flags) { - var cache = CreateTreeBuildCache(); + var cache = CreateTreeBuildCache(flags); foreach (var character in characters) { var tree = FromCharacter(character, cache, flags); @@ -63,7 +74,7 @@ public class ResourceTreeFactory( } public ResourceTree? FromCharacter(ICharacter character, Flags flags) - => FromCharacter(character, CreateTreeBuildCache(), flags); + => FromCharacter(character, CreateTreeBuildCache(flags), flags); private unsafe ResourceTree? FromCharacter(ICharacter character, TreeBuildCache cache, Flags flags) { @@ -80,7 +91,7 @@ public class ResourceTreeFactory( return null; var localPlayerRelated = cache.IsLocalPlayerRelated(character); - var (name, anonymizedName, related) = GetCharacterName(character); + var (name, anonymizedName, related) = GetCharacterName((GameObject*)character.Address); var networked = character.EntityId != 0xE0000000; var tree = new ResourceTree(name, anonymizedName, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related, networked, collectionResolveData.ModCollection.Identity.Name, collectionResolveData.ModCollection.Identity.AnonymizedName); @@ -183,36 +194,37 @@ public class ResourceTreeFactory( } } - private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(ICharacter character) + private unsafe (string Name, string AnonymizedName, bool PlayerRelated) GetCharacterName(GameObject* character) { - var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + var identifier = actors.FromObject(character, out var owner, true, false, false); var identifierStr = identifier.ToString(); return (identifierStr, identifier.Incognito(identifierStr), IsPlayerRelated(identifier, owner)); } - private unsafe bool IsPlayerRelated(ICharacter? character) + private unsafe bool IsPlayerRelated(GameObject* character) { - if (character == null) + if (character is null) return false; - var identifier = actors.FromObject((GameObject*)character.Address, out var owner, true, false, false); + var identifier = actors.FromObject(character, out var owner, true, false, false); return IsPlayerRelated(identifier, owner); } - private bool IsPlayerRelated(ActorIdentifier identifier, Actor owner) + private unsafe bool IsPlayerRelated(ActorIdentifier identifier, Actor owner) => identifier.Type switch { IdentifierType.Player => true, - IdentifierType.Owned => IsPlayerRelated(objects.Objects.CreateObjectReference(owner) as ICharacter), + IdentifierType.Owned => IsPlayerRelated(owner.AsObject), _ => false, }; [Flags] public enum Flags { - RedactExternalPaths = 1, - WithUiData = 2, - LocalPlayerRelatedOnly = 4, - WithOwnership = 8, + RedactExternalPaths = 1, + WithUiData = 2, + LocalPlayerRelatedOnly = 4, + WithOwnership = 8, + PopulateObjectTableData = 16, } } diff --git a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs index 49e00547..c0114412 100644 --- a/Penumbra/Interop/ResourceTree/TreeBuildCache.cs +++ b/Penumbra/Interop/ResourceTree/TreeBuildCache.cs @@ -12,14 +12,15 @@ using Penumbra.String.Classes; namespace Penumbra.Interop.ResourceTree; -internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager dataManager, ActorManager actors) +internal readonly struct TreeBuildCache(ObjectManager? objects, IDataManager dataManager, ActorManager actors) { private readonly Dictionary?> _shaderPackageNames = []; + private readonly IGameObject? _player = objects?.GetDalamudObject(0); + public unsafe bool IsLocalPlayerRelated(ICharacter character) { - var player = objects.GetDalamudObject(0); - if (player == null) + if (_player is null) return false; var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)character.Address; @@ -28,27 +29,26 @@ internal readonly struct TreeBuildCache(ObjectManager objects, IDataManager data return actualIndex switch { < 2 => true, - < (int)ScreenActor.CutsceneStart => gameObject->OwnerId == player.EntityId, + < (int)ScreenActor.CutsceneStart => gameObject->OwnerId == _player.EntityId, _ => false, }; } public IEnumerable GetCharacters() - => objects.Objects.OfType(); + => objects is not null ? objects.Objects.OfType() : []; public IEnumerable GetLocalPlayerRelatedCharacters() { - var player = objects.GetDalamudObject(0); - if (player == null) + if (_player is null) yield break; - yield return (ICharacter)player; + yield return (ICharacter)_player; - var minion = objects.GetDalamudObject(1); - if (minion != null) + var minion = objects!.GetDalamudObject(1); + if (minion is not null) yield return (ICharacter)minion; - var playerId = player.EntityId; + var playerId = _player.EntityId; for (var i = 2; i < ObjectIndex.CutsceneStart.Index; i += 2) { if (objects.GetDalamudObject(i) is ICharacter owned && owned.OwnerId == playerId) From b189ac027baa29fdeee79401985bd9968027d83b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 02:29:49 +0100 Subject: [PATCH 639/865] Fix imgui assert. --- Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs index 34aafbea..97761091 100644 --- a/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/DebugConfigurationDrawer.cs @@ -6,7 +6,8 @@ public static class DebugConfigurationDrawer { public static void Draw() { - if (!ImUtf8.CollapsingHeaderId("Debug Logging Options")) + using var id = ImUtf8.CollapsingHeaderId("Debug Logging Options"u8); + if (!id) return; ImUtf8.Checkbox("Log IMC File Replacements"u8, ref DebugConfiguration.WriteImcBytesToLog); From 8e191ae07525e6f05398a0bbbfa6ac5435232ae7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 13:33:43 +0100 Subject: [PATCH 640/865] Fix offsets. --- Penumbra/Interop/VolatileOffsets.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/VolatileOffsets.cs b/Penumbra/Interop/VolatileOffsets.cs index 2c6e3180..85008aae 100644 --- a/Penumbra/Interop/VolatileOffsets.cs +++ b/Penumbra/Interop/VolatileOffsets.cs @@ -6,7 +6,7 @@ public static class VolatileOffsets { public const int PlayTimeOffset = 0x254; public const int SomeIntermediate = 0x1F8; - public const int Flags = 0x4A4; + public const int Flags = 0x4A8; public const int IInstanceListenner = 0x270; public const int BitShift = 13; public const int CasterVFunc = 1; @@ -19,7 +19,7 @@ public static class VolatileOffsets public static class UpdateModel { - public const int ShortCircuit = 0xA2C; + public const int ShortCircuit = 0xA3C; } public static class FontReloader From 974b21561002c46461b1b81eb26aa32333b47060 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 13:58:11 +0100 Subject: [PATCH 641/865] 1.3.6.0 --- Penumbra.sln | 1 + Penumbra/UI/Changelog.cs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Penumbra.sln b/Penumbra.sln index ac1c9566..e52045b0 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .github\workflows\build.yml = .github\workflows\build.yml + Penumbra\Penumbra.json = Penumbra\Penumbra.json .github\workflows\release.yml = .github\workflows\release.yml repo.json = repo.json .github\workflows\test_release.yml = .github\workflows\test_release.yml diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 87dd101d..1b0225ed 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -59,10 +59,36 @@ public class PenumbraChangelog : IUiService Add1_3_3_0(Changelog); Add1_3_4_0(Changelog); Add1_3_5_0(Changelog); + Add1_3_6_0(Changelog); } #region Changelogs + private static void Add1_3_6_0(Changelog log) + => log.NextVersion("Version 1.3.6.0") + .RegisterImportant("Updated Penumbra for update 7.20 and Dalamud API 12.") + .RegisterEntry( + "This is not thoroughly tested, but I decided to push to stable instead of testing because otherwise a lot of people would just go to testing just for early access again despite having no business doing so.", + 1) + .RegisterEntry( + "I also do not use most of the functionality of Penumbra myself, so I am unable to even encounter most issues myself.", 1) + .RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1) + .RegisterImportant("There is a known issue with the Material Editor due to the shader changes, please do not author materials for the moment, they will be broken!", 1) + .RegisterHighlight( + "The texture editor now has encoding support for Block Compression 1, 4 and 5 and tooltips explaining when to use which format.") + .RegisterEntry("It also is able to use GPU compression and thus has become much faster for BC7 in particular. (Thanks Ny!)", 1) + .RegisterEntry( + "Added the option to import .atch files found in the particular mod via right-click context menu on the import drag & drop button.") + .RegisterEntry("Added a chat command to clear temporary settings done manually in Penumbra.") + .RegisterEntry( + "The changed item star to select the preferred changed item is a bit more noticeable by default, and its color can be configured.") + .RegisterEntry("Some minor fixes for computing changed items. (Thanks Anna!)") + .RegisterEntry("The EQP entry previously named Unknown 4 was renamed to 'Hide Glove Cuffs'.") + .RegisterEntry("Fixed the changed item identification for EST changes.") + .RegisterEntry("Fixed clipping issues in the changed items panel when no grouping was active."); + + + private static void Add1_3_5_0(Changelog log) => log.NextVersion("Version 1.3.5.0") .RegisterImportant( From 60becf0a090fa2a999cda123adaadac369ee13ec Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 14:06:21 +0100 Subject: [PATCH 642/865] Use staging build for release for now. --- .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 b019da2a8c370595a9f9190c961cd18592a0cabb Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 13:09:26 +0000 Subject: [PATCH 643/865] [CI] Updating repo.json for 1.3.6.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 1617a879..69db2f84 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.3.5.0", - "TestingAssemblyVersion": "1.3.5.1", + "AssemblyVersion": "1.3.6.0", + "TestingAssemblyVersion": "1.3.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 11, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.5.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.5.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 1a1d1c184026f363ddae1632d5c089b34c82cc0a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 14:10:52 +0100 Subject: [PATCH 644/865] Revert Dalamud staging on release, and update api level. --- .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 69db2f84..e4fd5abc 100644 --- a/repo.json +++ b/repo.json @@ -9,8 +9,8 @@ "TestingAssemblyVersion": "1.3.6.0", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 11, - "TestingDalamudApiLevel": 11, + "DalamudApiLevel": 12, + "TestingDalamudApiLevel": 12, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From 23ba77c107516a9f373d8f88cb71f421184f75f2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 15:52:40 +0100 Subject: [PATCH 645/865] Update build step and check for pre 7.2 shps. --- Penumbra.GameData | 2 +- .../Interop/Processing/ShpkPathPreProcessor.cs | 8 ++++---- Penumbra/Penumbra.csproj | 17 +++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 1158cf40..85921598 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1158cf404a16979d0b7e12f7bbcbbc651da16add +Subproject commit 859215989da41a4ccb59a5ce390223570a69c94e diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 2fb35ae0..826771dd 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -56,8 +56,8 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, using var file = MmioMemoryManager.CreateFromFile(path, access: MemoryMappedFileAccess.Read); var bytes = file.GetSpan(); - return ShpkFile.FastIsLegacy(bytes) - ? SanityCheckResult.Legacy + return ShpkFile.FastIsObsolete(bytes) + ? SanityCheckResult.Obsolete : SanityCheckResult.Success; } catch (FileNotFoundException) @@ -75,7 +75,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, { SanityCheckResult.IoError => "Cannot read the modded file.", SanityCheckResult.NotFound => "The modded file does not exist.", - SanityCheckResult.Legacy => "This mod is not compatible with Dawntrail. Get an updated version, if possible, or disable it.", + SanityCheckResult.Obsolete => "This mod is not compatible with Dawntrail post patch 7.2. Get an updated version, if possible, or disable it.", _ => string.Empty, }; @@ -84,6 +84,6 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, Success, IoError, NotFound, - Legacy, + Obsolete, } } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index cc892fa8..f93b1815 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,6 +23,13 @@ PreserveNewest + + PreserveNewest + DirectXTexC.dll + + + PreserveNewest + @@ -64,16 +71,6 @@ - - - PreserveNewest - DirectXTexC.dll - - - PreserveNewest - - - From 7498bc469f413dbe4d4230cb7ae64b98df4037b3 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 14:55:12 +0000 Subject: [PATCH 646/865] [CI] Updating repo.json for 1.3.6.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index e4fd5abc..94200e61 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.3.6.0", - "TestingAssemblyVersion": "1.3.6.0", + "AssemblyVersion": "1.3.6.1", + "TestingAssemblyVersion": "1.3.6.1", "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.3.6.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 01e6f5846335d3741395c741c3f244e71ef36446 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 16:53:43 +0100 Subject: [PATCH 647/865] Add Launching IPC Event. API 5.8 --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 5 ++++- Penumbra/Api/IpcLaunchingProvider.cs | 28 ++++++++++++++++++++++++++++ Penumbra/Penumbra.cs | 2 ++ 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 Penumbra/Api/IpcLaunchingProvider.cs diff --git a/Penumbra.Api b/Penumbra.Api index 2cbf4bac..bd56d828 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 2cbf4bace53a5749d3eab1ff03025a6e6bd9fc37 +Subproject commit bd56d82816b8366e19dddfb2dc7fd7f167e264ee diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 36f799a0..47d44cfc 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -16,13 +16,16 @@ public class PenumbraApi( TemporaryApi temporary, UiApi ui) : IDisposable, IApiService, IPenumbraApi { + public const int BreakingVersion = 5; + public const int FeatureVersion = 8; + public void Dispose() { Valid = false; } public (int Breaking, int Feature) ApiVersion - => (5, 7); + => (BreakingVersion, FeatureVersion); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/IpcLaunchingProvider.cs b/Penumbra/Api/IpcLaunchingProvider.cs new file mode 100644 index 00000000..ff851003 --- /dev/null +++ b/Penumbra/Api/IpcLaunchingProvider.cs @@ -0,0 +1,28 @@ +using Dalamud.Plugin; +using OtterGui.Log; +using OtterGui.Services; +using Penumbra.Api.Api; +using Serilog.Events; + +namespace Penumbra.Api; + +public sealed class IpcLaunchingProvider : IApiService +{ + public IpcLaunchingProvider(IDalamudPluginInterface pi, Logger log) + { + try + { + using var subscriber = log.MainLogger.IsEnabled(LogEventLevel.Debug) + ? IpcSubscribers.Launching.Subscriber(pi, + (major, minor) => log.Debug($"[IPC] Invoked Penumbra.Launching IPC with API Version {major}.{minor}.")) + : null; + + using var provider = IpcSubscribers.Launching.Provider(pi); + provider.Invoke(PenumbraApi.BreakingVersion, PenumbraApi.FeatureVersion); + } + catch (Exception ex) + { + log.Error($"[IPC] Could not invoke Penumbra.Launching IPC:\n{ex}"); + } + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b6009627..79c7f2db 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -58,6 +58,8 @@ public class Penumbra : IDalamudPlugin { HookOverrides.Instance = HookOverrides.LoadFile(pluginInterface); _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); + // Invoke the IPC Penumbra.Launching method before any hooks or other services are created. + _services.GetService(); Messager = _services.GetService(); _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); From 8a68a1bff52bb80a79f2041a54efb5b03905d1cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 17:25:03 +0100 Subject: [PATCH 648/865] Update GameData. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 85921598..e717a66f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 859215989da41a4ccb59a5ce390223570a69c94e +Subproject commit e717a66f33b0656a7c5c971ffa2f63fd96477d94 From 3bb7db10fba29088a83a0c0adf71b248de38552c Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 16:29:20 +0000 Subject: [PATCH 649/865] [CI] Updating repo.json for 1.3.6.2 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 94200e61..4fac86da 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.3.6.1", - "TestingAssemblyVersion": "1.3.6.1", + "AssemblyVersion": "1.3.6.2", + "TestingAssemblyVersion": "1.3.6.2", "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.3.6.1/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.1/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From a1bf26e7e8a00deafea2b09e41df8437a69641ae Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 28 Mar 2025 18:30:18 +0100 Subject: [PATCH 650/865] Run HTTP redraws on framework thread. --- Penumbra/Api/HttpApi.cs | 51 +++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/Penumbra/Api/HttpApi.cs b/Penumbra/Api/HttpApi.cs index 859c46b4..b6e1d799 100644 --- a/Penumbra/Api/HttpApi.cs +++ b/Penumbra/Api/HttpApi.cs @@ -1,3 +1,4 @@ +using Dalamud.Plugin.Services; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; @@ -14,7 +15,7 @@ public class HttpApi : IDisposable, IApiService // @formatter:off [Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods(); [Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw(); - [Route( HttpVerbs.Post, "/redrawAll" )] public partial void RedrawAll(); + [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(); @@ -24,11 +25,13 @@ public class HttpApi : IDisposable, IApiService public const string Prefix = "http://localhost:42069/"; private readonly IPenumbraApi _api; + private readonly IFramework _framework; private WebServer? _server; - public HttpApi(Configuration config, IPenumbraApi api) + public HttpApi(Configuration config, IPenumbraApi api, IFramework framework) { - _api = api; + _api = api; + _framework = framework; if (config.EnableHttpApi) CreateWebServer(); } @@ -44,7 +47,7 @@ public class HttpApi : IDisposable, IApiService .WithUrlPrefix(Prefix) .WithMode(HttpListenerMode.EmbedIO)) .WithCors(Prefix) - .WithWebApi("/api", m => m.WithController(() => new Controller(_api))); + .WithWebApi("/api", m => m.WithController(() => new Controller(_api, _framework))); _server.StateChanged += (_, e) => Penumbra.Log.Information($"WebServer New State - {e.NewState}"); _server.RunAsync(); @@ -59,60 +62,58 @@ public class HttpApi : IDisposable, IApiService public void Dispose() => ShutdownWebServer(); - private partial class Controller + private partial class Controller(IPenumbraApi api, IFramework framework) { - private readonly IPenumbraApi _api; - - public Controller(IPenumbraApi api) - => _api = api; - public partial object? GetMods() { Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered."); - return _api.Mods.GetModList(); + return api.Mods.GetModList(); } public async partial Task Redraw() { - var data = await HttpContext.GetRequestDataAsync(); - Penumbra.Log.Debug($"[HTTP] {nameof(Redraw)} triggered with {data}."); - if (data.ObjectTableIndex >= 0) - _api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); - else - _api.Redraw.RedrawAll(data.Type); + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); + Penumbra.Log.Debug($"[HTTP] [{Environment.CurrentManagedThreadId}] {nameof(Redraw)} triggered with {data}."); + await framework.RunOnFrameworkThread(() => + { + if (data.ObjectTableIndex >= 0) + api.Redraw.RedrawObject(data.ObjectTableIndex, data.Type); + else + api.Redraw.RedrawAll(data.Type); + }).ConfigureAwait(false); } - public partial void RedrawAll() + public async partial Task RedrawAll() { Penumbra.Log.Debug($"[HTTP] {nameof(RedrawAll)} triggered."); - _api.Redraw.RedrawAll(RedrawType.Redraw); + await framework.RunOnFrameworkThread(() => { api.Redraw.RedrawAll(RedrawType.Redraw); }).ConfigureAwait(false); } public async partial Task ReloadMod() { - var data = await HttpContext.GetRequestDataAsync(); + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); Penumbra.Log.Debug($"[HTTP] {nameof(ReloadMod)} triggered with {data}."); // Add the mod if it is not already loaded and if the directory name is given. // AddMod returns Success if the mod is already loaded. if (data.Path.Length != 0) - _api.Mods.AddMod(data.Path); + api.Mods.AddMod(data.Path); // Reload the mod by path or name, which will also remove no-longer existing mods. - _api.Mods.ReloadMod(data.Path, data.Name); + api.Mods.ReloadMod(data.Path, data.Name); } public async partial Task InstallMod() { - var data = await HttpContext.GetRequestDataAsync(); + var data = await HttpContext.GetRequestDataAsync().ConfigureAwait(false); Penumbra.Log.Debug($"[HTTP] {nameof(InstallMod)} triggered with {data}."); if (data.Path.Length != 0) - _api.Mods.InstallMod(data.Path); + api.Mods.InstallMod(data.Path); } public partial void OpenWindow() { Penumbra.Log.Debug($"[HTTP] {nameof(OpenWindow)} triggered."); - _api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); + api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty); } private record ModReloadData(string Path, string Name) From de408e4d58328686084a4f06e7e0924abb344011 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Mar 2025 17:33:26 +0000 Subject: [PATCH 651/865] [CI] Updating repo.json for 1.3.6.3 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 4fac86da..7a09af2e 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.3.6.2", - "TestingAssemblyVersion": "1.3.6.2", + "AssemblyVersion": "1.3.6.3", + "TestingAssemblyVersion": "1.3.6.3", "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.3.6.2/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.2/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 5a5a1487a31ceed61ef98160d7eca6e2645a2f2c Mon Sep 17 00:00:00 2001 From: Exter-N Date: Fri, 28 Mar 2025 20:24:22 +0100 Subject: [PATCH 652/865] Fix texture naming in Resource Trees --- Penumbra/Interop/ResourceTree/ResolveContext.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 41a27ed5..99360077 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -270,13 +270,13 @@ internal unsafe partial record ResolveContext( if (samplerId.HasValue) { alreadyProcessedSamplerIds.Add(samplerId.Value); - var samplerCrc = GetSamplerCrcById(shpk, samplerId.Value); - if (samplerCrc.HasValue) + var textureCrc = GetTextureCrcById(shpk, samplerId.Value); + if (textureCrc.HasValue) { - if (shpkNames != null && shpkNames.TryGetValue(samplerCrc.Value, out var samplerName)) + if (shpkNames != null && shpkNames.TryGetValue(textureCrc.Value, out var samplerName)) name = samplerName.Value; else - name = $"Texture 0x{samplerCrc.Value:X8}"; + name = $"Texture 0x{textureCrc.Value:X8}"; } } } @@ -292,9 +292,9 @@ internal unsafe partial record ResolveContext( return node; - static uint? GetSamplerCrcById(ShaderPackage* shpk, uint id) - => shpk->SamplersSpan.FindFirst(s => s.Id == id, out var s) - ? s.CRC + static uint? GetTextureCrcById(ShaderPackage* shpk, uint id) + => shpk->TexturesSpan.FindFirst(t => t.Id == id, out var t) + ? t.CRC : null; static uint? GetTextureSamplerId(Material* mtrl, TextureResourceHandle* handle, HashSet alreadyVisitedSamplerIds) From cb0214ca2ff22c3d7ad8445aef685415e3d66088 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 29 Mar 2025 16:52:08 +0100 Subject: [PATCH 653/865] Fix material editor and improve pinning logic --- Penumbra.GameData | 2 +- .../Import/Models/Export/MaterialExporter.cs | 4 +- .../Materials/MtrlTab.CommonColorTable.cs | 2 +- .../Materials/MtrlTab.ShaderPackage.cs | 8 +- .../Materials/MtrlTab.Textures.cs | 146 ++++++++++-------- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 19 ++- 6 files changed, 110 insertions(+), 71 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e717a66f..b6b91f84 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e717a66f33b0656a7c5c971ffa2f63fd96477d94 +Subproject commit b6b91f846096d15276b728ba2078f27b95317d15 diff --git a/Penumbra/Import/Models/Export/MaterialExporter.cs b/Penumbra/Import/Models/Export/MaterialExporter.cs index 121e6eed..6be2ccbd 100644 --- a/Penumbra/Import/Models/Export/MaterialExporter.cs +++ b/Penumbra/Import/Models/Export/MaterialExporter.cs @@ -288,7 +288,7 @@ public class MaterialExporter const uint valueFace = 0x6E5B8F10; var isFace = material.Mtrl.ShaderPackage.ShaderKeys - .Any(key => key is { Category: categoryHairType, Value: valueFace }); + .Any(key => key is { Key: categoryHairType, Value: valueFace }); var normal = material.Textures[TextureUsage.SamplerNormal]; var mask = material.Textures[TextureUsage.SamplerMask]; @@ -363,7 +363,7 @@ public class MaterialExporter // Face is the default for the skin shader, so a lack of skin type category is also correct. var isFace = !material.Mtrl.ShaderPackage.ShaderKeys - .Any(key => key.Category == categorySkinType && key.Value != valueFace); + .Any(key => key.Key == categorySkinType && key.Value != valueFace); // TODO: There's more nuance to skin than this, but this should be enough for a baseline reference. // TODO: Specular? diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 236a66c3..d70a4b50 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -22,7 +22,7 @@ public partial class MtrlTab private bool DrawColorTableSection(bool disabled) { - if (!_shpkLoading && !SamplerIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) + if (!_shpkLoading && !TextureIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) return false; ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index a13dd96b..202047e4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -216,7 +216,7 @@ public partial class MtrlTab else foreach (var (key, index) in Mtrl.ShaderPackage.ShaderKeys.WithIndex()) { - var keyName = Names.KnownNames.TryResolve(key.Category); + var keyName = Names.KnownNames.TryResolve(key.Key); var valueName = keyName.WithKnownSuffixes().TryResolve(Names.KnownNames, key.Value); _shaderKeys.Add((keyName.ToString(), index, string.Empty, true, [(valueName.ToString(), key.Value, string.Empty)])); } @@ -366,6 +366,7 @@ public partial class MtrlTab ret = true; _associatedShpk = null; _loadedShpkPath = FullPath.Empty; + UnpinResources(true); LoadShpk(FindAssociatedShpk(out _, out _)); } @@ -442,8 +443,8 @@ public partial class MtrlTab { using var font = ImRaii.PushFont(UiBuilder.MonoFont, monoFont); ref var key = ref Mtrl.ShaderPackage.ShaderKeys[index]; - using var id = ImUtf8.PushId((int)key.Category); - var shpkKey = _associatedShpk?.GetMaterialKeyById(key.Category); + using var id = ImUtf8.PushId((int)key.Key); + var shpkKey = _associatedShpk?.GetMaterialKeyById(key.Key); var currentValue = key.Value; var (currentLabel, _, currentDescription) = values.FirstOrNull(v => v.Value == currentValue) ?? ($"0x{currentValue:X8}", currentValue, string.Empty); @@ -459,6 +460,7 @@ public partial class MtrlTab { key.Value = value; ret = true; + UnpinResources(false); Update(); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 7ab2900d..dfa07d52 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -3,7 +3,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Text; -using Penumbra.GameData; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.String.Classes; using static Penumbra.GameData.Files.MaterialStructs.SamplerFlags; @@ -16,18 +15,22 @@ public partial class MtrlTab public readonly List<(string Label, int TextureIndex, int SamplerIndex, string Description, bool MonoFont)> Textures = new(4); public readonly HashSet UnfoldedTextures = new(4); + public readonly HashSet TextureIds = new(16); public readonly HashSet SamplerIds = new(16); public float TextureLabelWidth; + private bool _samplersPinned; private void UpdateTextures() { Textures.Clear(); + TextureIds.Clear(); SamplerIds.Clear(); if (_associatedShpk == null) { + TextureIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); if (Mtrl.Table != null) - SamplerIds.Add(TableSamplerId); + TextureIds.Add(TableSamplerId); foreach (var (sampler, index) in Mtrl.ShaderPackage.Samplers.WithIndex()) Textures.Add(($"0x{sampler.SamplerId:X8}", sampler.TextureIndex, index, string.Empty, true)); @@ -35,31 +38,39 @@ public partial class MtrlTab else { foreach (var index in _vertexShaders) - SamplerIds.UnionWith(_associatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); - foreach (var index in _pixelShaders) - SamplerIds.UnionWith(_associatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); - if (!_shadersKnown) { - SamplerIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); - if (Mtrl.Table != null) - SamplerIds.Add(TableSamplerId); + TextureIds.UnionWith(_associatedShpk.VertexShaders[index].Textures.Select(texture => texture.Id)); + SamplerIds.UnionWith(_associatedShpk.VertexShaders[index].Samplers.Select(sampler => sampler.Id)); } - foreach (var samplerId in SamplerIds) + foreach (var index in _pixelShaders) { - var shpkSampler = _associatedShpk.GetSamplerById(samplerId); - if (shpkSampler is not { Slot: 2 }) + TextureIds.UnionWith(_associatedShpk.PixelShaders[index].Textures.Select(texture => texture.Id)); + SamplerIds.UnionWith(_associatedShpk.PixelShaders[index].Samplers.Select(sampler => sampler.Id)); + } + + if (_samplersPinned || !_shadersKnown) + { + TextureIds.UnionWith(Mtrl.ShaderPackage.Samplers.Select(sampler => sampler.SamplerId)); + if (Mtrl.Table != null) + TextureIds.Add(TableSamplerId); + } + + foreach (var textureId in TextureIds) + { + var shpkTexture = _associatedShpk.GetTextureById(textureId); + if (shpkTexture is not { Slot: 2 }) continue; - var dkData = TryGetShpkDevkitData("Samplers", samplerId, true); + var dkData = TryGetShpkDevkitData("Samplers", textureId, true); var hasDkLabel = !string.IsNullOrEmpty(dkData?.Label); - var sampler = Mtrl.GetOrAddSampler(samplerId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); - Textures.Add((hasDkLabel ? dkData!.Label : shpkSampler.Value.Name, sampler.TextureIndex, samplerIndex, + var sampler = Mtrl.GetOrAddSampler(textureId, dkData?.DefaultTexture ?? string.Empty, out var samplerIndex); + Textures.Add((hasDkLabel ? dkData!.Label : shpkTexture.Value.Name, sampler.TextureIndex, samplerIndex, dkData?.Description ?? string.Empty, !hasDkLabel)); } - if (SamplerIds.Contains(TableSamplerId)) + if (TextureIds.Contains(TableSamplerId)) Mtrl.Table ??= new ColorTable(); } @@ -205,58 +216,67 @@ public partial class MtrlTab ret = true; } - ref var samplerFlags = ref Wrap(ref sampler.Flags); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - var addressMode = samplerFlags.UAddressMode; - if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + if (SamplerIds.Contains(sampler.SamplerId)) { - samplerFlags.UAddressMode = addressMode; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); + ref var samplerFlags = ref Wrap(ref sampler.Flags); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + var addressMode = samplerFlags.UAddressMode; + if (ComboTextureAddressMode("##UAddressMode"u8, ref addressMode)) + { + samplerFlags.UAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("U Address Mode"u8, + "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); + + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + addressMode = samplerFlags.VAddressMode; + if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + { + samplerFlags.VAddressMode = addressMode; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("V Address Mode"u8, + "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); + + var lodBias = samplerFlags.LodBias; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) + { + samplerFlags.LodBias = lodBias; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, + "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); + + var minLod = samplerFlags.MinLod; + ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); + if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) + { + samplerFlags.MinLod = minLod; + ret = true; + SetSamplerFlags(sampler.SamplerId, sampler.Flags); + } + + ImGui.SameLine(); + ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, + "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); } - - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("U Address Mode"u8, "Method to use for resolving a U texture coordinate that is outside the 0 to 1 range."); - - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - addressMode = samplerFlags.VAddressMode; - if (ComboTextureAddressMode("##VAddressMode"u8, ref addressMode)) + else { - samplerFlags.VAddressMode = addressMode; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); + ImUtf8.Text("This texture does not have a dedicated sampler."u8); } - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("V Address Mode"u8, "Method to use for resolving a V texture coordinate that is outside the 0 to 1 range."); - - var lodBias = samplerFlags.LodBias; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImUtf8.DragScalar("##LoDBias"u8, ref lodBias, -8.0f, 7.984375f, 0.1f)) - { - samplerFlags.LodBias = lodBias; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("Level of Detail Bias"u8, - "Offset from the calculated mipmap level.\n\nHigher means that the texture will start to lose detail nearer.\nLower means that the texture will keep its detail until farther."); - - var minLod = samplerFlags.MinLod; - ImGui.SetNextItemWidth(UiHelpers.Scale * 100.0f); - if (ImUtf8.DragScalar("##MinLoD"u8, ref minLod, 0, 15, 0.1f)) - { - samplerFlags.MinLod = minLod; - ret = true; - SetSamplerFlags(sampler.SamplerId, sampler.Flags); - } - - ImGui.SameLine(); - ImUtf8.LabeledHelpMarker("Minimum Level of Detail"u8, - "Most detailed mipmap level to use.\n\n0 is the full-sized texture, 1 is the half-sized texture, 2 is the quarter-sized texture, and so on.\n15 will forcibly reduce the texture to its smallest mipmap."); - using var t = ImUtf8.TreeNode("Advanced Settings"u8); if (!t) return ret; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 6e16de99..97acf130 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -57,6 +57,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable Mtrl = file; FilePath = filePath; Writable = writable; + _samplersPinned = true; _associatedBaseDevkit = TryLoadShpkDevkit("_base", out _loadedBaseDevkitPathName); Update(); LoadShpk(FindAssociatedShpk(out _, out _)); @@ -172,6 +173,22 @@ public sealed partial class MtrlTab : IWritable, IDisposable Widget.DrawHexViewer(Mtrl.AdditionalData); } + private void UnpinResources(bool all) + { + _samplersPinned = false; + + if (!all) + return; + + var keys = Mtrl.ShaderPackage.ShaderKeys; + for (var i = 0; i < keys.Length; i++) + keys[i].Pinned = false; + + var constants = Mtrl.ShaderPackage.Constants; + for (var i = 0; i < constants.Length; i++) + constants[i].Pinned = false; + } + private void Update() { UpdateShaders(); @@ -192,7 +209,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable public byte[] Write() { var output = Mtrl.Clone(); - output.GarbageCollect(_associatedShpk, SamplerIds); + output.GarbageCollect(_associatedShpk, TextureIds); return output.Write(); } From 2dd6dd201c61f80a1ce3a01088120b3c5adc7aff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Mar 2025 18:03:50 +0100 Subject: [PATCH 654/865] Update PAP records. --- Penumbra/UI/ResourceWatcher/Record.cs | 20 +++++++++++++++++++ .../UI/ResourceWatcher/ResourceWatcher.cs | 12 +++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 8ab96f4b..b8730750 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -56,6 +56,26 @@ internal unsafe struct Record Crc64 = 0, }; + public static Record CreateRequest(CiByteString path, bool sync, FullPath fullPath, ResolveData resolve) + => new() + { + Time = DateTime.UtcNow, + Path = fullPath.InternalName.IsOwned ? fullPath.InternalName : fullPath.InternalName.Clone(), + OriginalPath = path.IsOwned ? path : path.Clone(), + Collection = resolve.Valid ? resolve.ModCollection : null, + Handle = null, + ResourceType = ResourceExtensions.Type(path).ToFlag(), + Category = ResourceExtensions.Category(path).ToFlag(), + RefCount = 0, + RecordType = RecordType.Request, + Synchronously = sync, + ReturnValue = OptionalBool.Null, + CustomLoad = fullPath.InternalName != path, + AssociatedGameObject = string.Empty, + LoadState = LoadState.None, + Crc64 = fullPath.Crc64, + }; + public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { path = path.IsOwned ? path : path.Clone(); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index 94bd4307..d134cfe5 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -58,12 +58,19 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService private void OnPapRequested(Utf8GamePath original, FullPath? _1, ResolveData _2) { if (_ephemeral.EnableResourceLogging && FilterMatch(original.Path, out var match)) + { Penumbra.Log.Information($"[ResourceLoader] [REQ] {match} was requested asynchronously."); + if (_1.HasValue) + Penumbra.Log.Information( + $"[ResourceLoader] [LOAD] Resolved {_1.Value.FullName} for {match} from collection {_2.ModCollection} for object 0x{_2.AssociatedGameObject:X}."); + } if (!_ephemeral.EnableResourceWatcher) return; - var record = Record.CreateRequest(original.Path, false); + var record = _1.HasValue + ? Record.CreateRequest(original.Path, false, _1.Value, _2) + : Record.CreateRequest(original.Path, false); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) _newRecords.Enqueue(record); } @@ -257,7 +264,8 @@ public sealed class ResourceWatcher : IDisposable, ITab, IUiService _newRecords.Enqueue(record); } - private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, ReadOnlySpan additionalData, bool isAsync) + private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, + ReadOnlySpan additionalData, bool isAsync) { if (!isAsync) return; From f3bcc4d55492f422384099c1dadac50968ce3ba0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 29 Mar 2025 18:05:47 +0100 Subject: [PATCH 655/865] Update changelog. --- Penumbra/UI/Changelog.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 1b0225ed..32abeb41 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -60,10 +60,15 @@ public class PenumbraChangelog : IUiService Add1_3_4_0(Changelog); Add1_3_5_0(Changelog); Add1_3_6_0(Changelog); + Add1_3_6_4(Changelog); } #region Changelogs + private static void Add1_3_6_4(Changelog log) + => log.NextVersion("Version 1.3.6.4") + .RegisterEntry("The material editor should be functional again."); + private static void Add1_3_6_0(Changelog log) => log.NextVersion("Version 1.3.6.0") .RegisterImportant("Updated Penumbra for update 7.20 and Dalamud API 12.") @@ -73,7 +78,6 @@ public class PenumbraChangelog : IUiService .RegisterEntry( "I also do not use most of the functionality of Penumbra myself, so I am unable to even encounter most issues myself.", 1) .RegisterEntry("If you encounter any issues, please report them quickly on the discord.", 1) - .RegisterImportant("There is a known issue with the Material Editor due to the shader changes, please do not author materials for the moment, they will be broken!", 1) .RegisterHighlight( "The texture editor now has encoding support for Block Compression 1, 4 and 5 and tooltips explaining when to use which format.") .RegisterEntry("It also is able to use GPU compression and thus has become much faster for BC7 in particular. (Thanks Ny!)", 1) From cc76125b1c3f5c1e60e3fd3e0b344e01add3a8d9 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 29 Mar 2025 17:07:46 +0000 Subject: [PATCH 656/865] [CI] Updating repo.json for 1.3.6.4 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 7a09af2e..17fe95b3 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.3.6.3", - "TestingAssemblyVersion": "1.3.6.3", + "AssemblyVersion": "1.3.6.4", + "TestingAssemblyVersion": "1.3.6.4", "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.3.6.3/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.3/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From b589103b05d19122c6a9f01d9e8ceb9f33a7510f Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 30 Mar 2025 13:44:04 +0200 Subject: [PATCH 657/865] Make resolvedData thread-local --- .../Hooks/ResourceLoading/ResourceLoader.cs | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs index 3f8cb23f..6ddcbfda 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/ResourceLoader.cs @@ -23,7 +23,7 @@ public unsafe class ResourceLoader : IDisposable, IService private readonly ConcurrentDictionary _ongoingLoads = []; - private ResolveData _resolvedData = ResolveData.Invalid; + private readonly ThreadLocal _resolvedData = new(() => ResolveData.Invalid); public event Action? PapRequested; public IReadOnlyDictionary OngoingLoads @@ -56,10 +56,11 @@ public unsafe class ResourceLoader : IDisposable, IService if (!_config.EnableMods || !Utf8GamePath.FromPointer(path, MetaDataComputation.CiCrc32, out var gamePath)) return length; + var resolvedData = _resolvedData.Value; var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) - : _resolvedData.Valid - ? (_resolvedData.ModCollection.ResolvePath(gamePath), _resolvedData) + : resolvedData.Valid + ? (resolvedData.ModCollection.ResolvePath(gamePath), resolvedData) : ResolvePath(gamePath, ResourceCategory.Chara, ResourceType.Pap); @@ -78,19 +79,31 @@ public unsafe class ResourceLoader : IDisposable, IService /// Load a resource for a given path and a specific collection. public ResourceHandle* LoadResolvedResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { - _resolvedData = resolveData; - var ret = _resources.GetResource(category, type, path); - _resolvedData = ResolveData.Invalid; - return ret; + var previous = _resolvedData.Value; + _resolvedData.Value = resolveData; + try + { + return _resources.GetResource(category, type, path); + } + finally + { + _resolvedData.Value = previous; + } } /// Load a resource for a given path and a specific collection. public SafeResourceHandle LoadResolvedSafeResource(ResourceCategory category, ResourceType type, CiByteString path, ResolveData resolveData) { - _resolvedData = resolveData; - var ret = _resources.GetSafeResource(category, type, path); - _resolvedData = ResolveData.Invalid; - return ret; + var previous = _resolvedData.Value; + _resolvedData.Value = resolveData; + try + { + return _resources.GetSafeResource(category, type, path); + } + finally + { + _resolvedData.Value = previous; + } } /// The function to use to resolve a given path. @@ -159,10 +172,11 @@ public unsafe class ResourceLoader : IDisposable, IService CompareHash(ComputeHash(path.Path, parameters), hash, path); // If no replacements are being made, we still want to be able to trigger the event. + var resolvedData = _resolvedData.Value; var (resolvedPath, data) = _incMode.Value ? (null, ResolveData.Invalid) - : _resolvedData.Valid - ? (_resolvedData.ModCollection.ResolvePath(path), _resolvedData) + : resolvedData.Valid + ? (resolvedData.ModCollection.ResolvePath(path), resolvedData) : ResolvePath(path, category, type); if (resolvedPath == null || !Utf8GamePath.FromByteString(resolvedPath.Value.InternalName, out var p)) From fe5d1bc36ee05017fde0bf9cdad71772fd1ad109 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 30 Mar 2025 16:08:59 +0000 Subject: [PATCH 658/865] [CI] Updating repo.json for 1.3.6.5 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 17fe95b3..2c2088c3 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.3.6.4", - "TestingAssemblyVersion": "1.3.6.4", + "AssemblyVersion": "1.3.6.5", + "TestingAssemblyVersion": "1.3.6.5", "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.3.6.4/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.4/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 1d517103b3d52758afbd47648352d793446eb5c6 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 30 Mar 2025 19:55:43 +0200 Subject: [PATCH 659/865] Mtrl editor: Fix texture pinning --- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index dfa07d52..dd01ec2b 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -59,7 +59,7 @@ public partial class MtrlTab foreach (var textureId in TextureIds) { var shpkTexture = _associatedShpk.GetTextureById(textureId); - if (shpkTexture is not { Slot: 2 }) + if (shpkTexture is not { Slot: 2 } && (shpkTexture is not null || textureId == TableSamplerId)) continue; var dkData = TryGetShpkDevkitData("Samplers", textureId, true); From abb47751c821b86eb8f7a3e88a9e12939a611d3a Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 30 Mar 2025 20:29:25 +0200 Subject: [PATCH 660/865] Mtrl editor: Disregard obsolete modded ShPks --- Penumbra/Interop/Processing/ShpkPathPreProcessor.cs | 4 ++-- .../UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs index 826771dd..ddd59121 100644 --- a/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs +++ b/Penumbra/Interop/Processing/ShpkPathPreProcessor.cs @@ -49,7 +49,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, return null; } - private static SanityCheckResult SanityCheck(string path) + internal static SanityCheckResult SanityCheck(string path) { try { @@ -79,7 +79,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager, _ => string.Empty, }; - private enum SanityCheckResult + internal enum SanityCheckResult { Success, IoError, diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index 202047e4..b76cffc2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -10,6 +10,7 @@ using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ShaderStructs; +using Penumbra.Interop.Processing; using Penumbra.String.Classes; using static Penumbra.GameData.Files.ShpkFile; @@ -128,7 +129,11 @@ public partial class MtrlTab if (!Utf8GamePath.FromString(defaultPath, out defaultGamePath)) return FullPath.Empty; - return _edit.FindBestMatch(defaultGamePath); + var path = _edit.FindBestMatch(defaultGamePath); + if (!path.IsRooted || ShpkPathPreProcessor.SanityCheck(path.FullName) == ShpkPathPreProcessor.SanityCheckResult.Success) + return path; + + return new FullPath(defaultPath); } private void LoadShpk(FullPath path) From c3be151d4023d6b4edc5985c259bfef8645cb4b7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 2 Apr 2025 23:36:56 +0200 Subject: [PATCH 661/865] Maybe fix crash issue in AtchHook1 / issue with kept draw object links. --- .../Hooks/Objects/CharacterDestructor.cs | 3 ++ .../Interop/PathResolving/DrawObjectState.cs | 36 ++++++++++++++++++- Penumbra/UI/Changelog.cs | 1 - 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs index ffe2f72d..55b392ba 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterDestructor.cs @@ -15,6 +15,9 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr IdentifiedCollectionCache = 0, + + /// + DrawObjectState = 0, } public CharacterDestructor(HookManager hooks) diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 5e413fe2..28a0dd8d 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -15,6 +15,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject = []; @@ -23,21 +24,24 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _gameState.LastGameObject; public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload, - CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework) + CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework, CharacterDestructor characterDestructor) { _objects = objects; _createCharacterBase = createCharacterBase; _weaponReload = weaponReload; _characterBaseDestructor = characterBaseDestructor; _gameState = gameState; + _characterDestructor = characterDestructor; framework.RunOnFrameworkThread(InitializeDrawObjects); _weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState); _weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState); _createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState); _characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState); + _characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.DrawObjectState); } + public bool ContainsKey(nint key) => _drawObjectToGameObject.ContainsKey(key); @@ -68,6 +72,36 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary + /// Seems like sometimes the draw object of a game object is destroyed in frames after the original game object is already destroyed. + /// So protect against outdated game object pointers in the dictionary. + /// + private unsafe void OnCharacterDestructor(Character* a) + { + if (a is null) + return; + + var character = (nint)a; + var delete = stackalloc nint[5]; + var current = 0; + foreach (var (drawObject, (gameObject, _)) in _drawObjectToGameObject) + { + if (gameObject != character) + continue; + + delete[current++] = drawObject; + if (current is 4) + break; + } + + for (var ptr = delete; *ptr != nint.Zero; ++ptr) + { + _drawObjectToGameObject.Remove(*ptr, out var pair); + Penumbra.Log.Excessive($"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject:X}, {pair.IsChild})."); + } } private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 32abeb41..5e1612eb 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -90,7 +90,6 @@ public class PenumbraChangelog : IUiService .RegisterEntry("The EQP entry previously named Unknown 4 was renamed to 'Hide Glove Cuffs'.") .RegisterEntry("Fixed the changed item identification for EST changes.") .RegisterEntry("Fixed clipping issues in the changed items panel when no grouping was active."); - private static void Add1_3_5_0(Changelog log) From 09c2264de4bd93eec422fc4e69e9e40e0b8023f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 2 Apr 2025 23:41:08 +0200 Subject: [PATCH 662/865] Revert overeager BNPC Name update. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index b6b91f84..ab63da80 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b6b91f846096d15276b728ba2078f27b95317d15 +Subproject commit ab63da8047f3d99240159bb1b17dbcb61d77326a From 2fdafc5c8581792acdd09c4a6e93bf8442b8edfa Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 2 Apr 2025 21:45:19 +0000 Subject: [PATCH 663/865] [CI] Updating repo.json for 1.3.6.6 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 2c2088c3..f471e95e 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.3.6.5", - "TestingAssemblyVersion": "1.3.6.5", + "AssemblyVersion": "1.3.6.6", + "TestingAssemblyVersion": "1.3.6.6", "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.3.6.5/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.5/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From c3b2443ab52231658bfa73e36d0d42e671cc2c79 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 4 Apr 2025 22:35:23 +0200 Subject: [PATCH 664/865] Add Incognito modifier. --- Penumbra/Configuration.cs | 1 + Penumbra/EphemeralConfig.cs | 1 + .../Hooks/Objects/CharacterBaseDestructor.cs | 5 ++--- .../Hooks/Objects/CharacterDestructor.cs | 2 +- .../Materials/MtrlTab.Textures.cs | 16 ++++++++-------- Penumbra/UI/Classes/CollectionSelectHeader.cs | 6 +++--- Penumbra/UI/IncognitoService.cs | 17 +++++++++++++---- Penumbra/UI/Tabs/SettingsTab.cs | 8 ++++++++ 8 files changed, 37 insertions(+), 19 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 939eb122..3a9bcdc4 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -94,6 +94,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService public string QuickMoveFolder2 { get; set; } = string.Empty; public string QuickMoveFolder3 { get; set; } = string.Empty; public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control); public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool AutoDeduplicateOnImport { get; set; } = true; public bool AutoReduplicateUiOnImport { get; set; } = true; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 24ab466b..678e53ad 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -41,6 +41,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService public string LastModPath { get; set; } = string.Empty; public bool AdvancedEditingOpen { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false; + public bool IncognitoMode { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs index 7636718e..2d8e60b2 100644 --- a/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs +++ b/Penumbra/Interop/Hooks/Objects/CharacterBaseDestructor.cs @@ -2,7 +2,6 @@ using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using OtterGui.Classes; using OtterGui.Services; -using Penumbra.UI.AdvancedWindow; namespace Penumbra.Interop.Hooks.Objects; @@ -13,7 +12,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr DrawObjectState = 0, - /// + /// MtrlTab = -1000, } @@ -42,7 +41,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr 0) - ImGuiUtil.LabeledHelpMarker(label, description); - else - ImGui.TextUnformatted(label); + using (ImRaii.PushFont(UiBuilder.MonoFont, monoFont)) + { + ImGui.AlignTextToFramePadding(); + if (description.Length > 0) + ImGuiUtil.LabeledHelpMarker(label, description); + else + ImGui.TextUnformatted(label); } if (unfolded) diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 54fcf279..d7a81876 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -61,7 +61,7 @@ public class CollectionSelectHeader : IUiService private void DrawTemporaryCheckbox() { - var hold = _config.DeleteModModifier.IsActive(); + var hold = _config.IncognitoModifier.IsActive(); using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) { var tint = ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint); @@ -77,9 +77,9 @@ public class CollectionSelectHeader : IUiService } ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, - "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired.\n"u8); + "Toggle the temporary settings mode, where all changes you do create temporary settings first and need to be made permanent if desired."u8); if (!hold) - ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"Hold {_config.DeleteModModifier} while clicking to toggle."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {_config.IncognitoModifier} while clicking to toggle."); } private enum CollectionState diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/IncognitoService.cs index d58ea1ec..29358618 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/IncognitoService.cs @@ -1,4 +1,5 @@ using Dalamud.Interface; +using ImGuiNET; using Penumbra.UI.Classes; using OtterGui.Raii; using OtterGui.Services; @@ -6,19 +7,27 @@ using OtterGui.Text; namespace Penumbra.UI; -public class IncognitoService(TutorialService tutorial) : IService +public class IncognitoService(TutorialService tutorial, Configuration config) : IService { - public bool IncognitoMode; + public bool IncognitoMode + => config.Ephemeral.IncognitoMode; public void DrawToggle(float width) { + var hold = config.IncognitoModifier.IsActive(); var color = ColorId.FolderExpanded.Value(); using (ImRaii.PushFrameBorder(ImUtf8.GlobalScale, color)) { var tt = IncognitoMode ? "Toggle incognito mode off."u8 : "Toggle incognito mode on."u8; var icon = IncognitoMode ? FontAwesomeIcon.EyeSlash : FontAwesomeIcon.Eye; - if (ImUtf8.IconButton(icon, tt, new Vector2(width, ImUtf8.FrameHeight), false, color)) - IncognitoMode = !IncognitoMode; + if (ImUtf8.IconButton(icon, tt, new Vector2(width, ImUtf8.FrameHeight), false, color) && hold) + { + config.Ephemeral.IncognitoMode = !IncognitoMode; + config.Ephemeral.Save(); + } + + if (!hold) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); } tutorial.OpenTutorial(BasicTutorialSteps.Incognito); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ba226aa8..b1f82a91 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -621,6 +621,14 @@ public class SettingsTab : ITab, IUiService _config.DeleteModModifier = v; _config.Save(); }); + Widget.DoubleModifierSelector("Incognito Modifier", + "A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", UiHelpers.InputTextWidth.X, + _config.IncognitoModifier, + v => + { + _config.IncognitoModifier = v; + _config.Save(); + }); } /// Draw all settings pertaining to import and export of mods. From 3b54485127dcbee68733d9eb5a5027c90e552619 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Apr 2025 14:42:25 +0200 Subject: [PATCH 665/865] Maybe fix AtchCaller crashes. --- Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs | 2 +- Penumbra/Interop/PathResolving/CollectionResolver.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs index c350c157..2a3d7468 100644 --- a/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs +++ b/Penumbra/Interop/Hooks/Meta/AtchCallerHook1.cs @@ -25,7 +25,7 @@ public unsafe class AtchCallerHook1 : FastHook, IDispo private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel) { - var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true); + var collection = playerModel.Valid ? _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true) : _collectionResolver.DefaultCollection; _metaState.AtchCollection.Push(collection); Task.Result.Original(data, slot, unk, playerModel); _metaState.AtchCollection.Pop(); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 576b61bb..f14abbff 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -95,6 +95,10 @@ public sealed unsafe class CollectionResolver( return IdentifyCollection(obj, useCache); } + /// Get the default collection. + public ResolveData DefaultCollection + => collectionManager.Active.Default.ToResolveData(); + /// Return whether the given ModelChara id refers to a human-type model. public bool IsModelHuman(uint modelCharaId) => humanModels.IsHuman(modelCharaId); From 5437ab477f349682edb09781340f6db34fbc9be2 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 5 Apr 2025 12:44:47 +0000 Subject: [PATCH 666/865] [CI] Updating repo.json for 1.3.6.7 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f471e95e..5163bb7d 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.3.6.6", - "TestingAssemblyVersion": "1.3.6.6", + "AssemblyVersion": "1.3.6.7", + "TestingAssemblyVersion": "1.3.6.7", "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.3.6.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.6/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 93e60471de1b68cfc828fba0819b4d7ab06072cc Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Apr 2025 18:49:18 +0200 Subject: [PATCH 667/865] Update for new objectmanager. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 3396ee17..f53fd227 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3396ee176fa72ad2dfb2de3294f7125ebce4dae5 +Subproject commit f53fd227a242435ce44a9fe9c5e847d0ca788869 diff --git a/Penumbra.GameData b/Penumbra.GameData index ab63da80..4769bbcd 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit ab63da8047f3d99240159bb1b17dbcb61d77326a +Subproject commit 4769bbcdfce9e1d5a461c6b552b5b30ad6bc478e diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 42502290..6629c126 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -503,6 +503,8 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; + _objects.DrawDebug(); + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) From 0afcae45046c5e4b25b59757296500aa92d4aae2 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 5 Apr 2025 18:49:30 +0200 Subject: [PATCH 668/865] Run API redraws on framework. --- Penumbra/Api/Api/RedrawApi.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Penumbra/Api/Api/RedrawApi.cs b/Penumbra/Api/Api/RedrawApi.cs index 82d14f7b..ec4de892 100644 --- a/Penumbra/Api/Api/RedrawApi.cs +++ b/Penumbra/Api/Api/RedrawApi.cs @@ -1,23 +1,32 @@ using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin.Services; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Interop.Services; namespace Penumbra.Api.Api; -public class RedrawApi(RedrawService redrawService) : IPenumbraApiRedraw, IApiService +public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService { public void RedrawObject(int gameObjectIndex, RedrawType setting) - => redrawService.RedrawObject(gameObjectIndex, setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting)); + } public void RedrawObject(string name, RedrawType setting) - => redrawService.RedrawObject(name, setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting)); + } public void RedrawObject(IGameObject? gameObject, RedrawType setting) - => redrawService.RedrawObject(gameObject, setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting)); + } public void RedrawAll(RedrawType setting) - => redrawService.RedrawAll(setting); + { + framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting)); + } public event GameObjectRedrawnDelegate? GameObjectRedrawn { From 33ada1d9949b6b68315e0e6f96f6714973250128 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 8 Apr 2025 16:56:23 +0200 Subject: [PATCH 669/865] Remove meta-default-value checking from TT imports, move it entirely to mod loads, and keep default-valued entries if other options actually edit the same entry. --- .editorconfig | 12 ++ OtterGui | 2 +- .../Import/TexToolsMeta.Deserialization.cs | 31 +--- Penumbra/Import/TexToolsMeta.Rgsp.cs | 7 +- Penumbra/Import/TexToolsMeta.cs | 9 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 162 ++++++++++++++++-- Penumbra/Mods/Manager/ModMigration.cs | 10 +- Penumbra/Mods/ModCreator.cs | 36 ++-- 8 files changed, 194 insertions(+), 75 deletions(-) diff --git a/.editorconfig b/.editorconfig index c645b573..f0328fd7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3576,6 +3576,18 @@ resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_ resharper_xaml_x_key_attribute_disallowed_highlighting=error resharper_xml_doc_comment_syntax_problem_highlighting=warning resharper_xunit_xunit_test_with_console_output_highlighting=warning +csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion +csharp_style_expression_bodied_methods = true:silent +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_expression_bodied_properties = true:silent [*.{cshtml,htm,html,proto,razor}] indent_style=tab diff --git a/OtterGui b/OtterGui index f53fd227..21ddfccb 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f53fd227a242435ce44a9fe9c5e847d0ca788869 +Subproject commit 21ddfccb91ba3fa56e1c191e706ff91bffaa9515 diff --git a/Penumbra/Import/TexToolsMeta.Deserialization.cs b/Penumbra/Import/TexToolsMeta.Deserialization.cs index 1f970dfe..7861a95b 100644 --- a/Penumbra/Import/TexToolsMeta.Deserialization.cs +++ b/Penumbra/Import/TexToolsMeta.Deserialization.cs @@ -2,7 +2,6 @@ using Lumina.Extensions; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Import.Structs; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -19,9 +18,7 @@ public partial class TexToolsMeta var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot); var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask; - var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask; - if (_keepDefault || def != value) - MetaManipulations.TryAdd(identifier, value); + MetaManipulations.TryAdd(identifier, value); } // Deserialize and check Eqdp Entries and add them to the list if they are non-default. @@ -41,11 +38,9 @@ public partial class TexToolsMeta continue; var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr); - var mask = Eqdp.Mask(metaFileInfo.EquipSlot); - var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask; - var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask; - if (_keepDefault || def != value) - MetaManipulations.TryAdd(identifier, value); + var mask = Eqdp.Mask(metaFileInfo.EquipSlot); + var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask; + MetaManipulations.TryAdd(identifier, value); } } @@ -55,10 +50,9 @@ public partial class TexToolsMeta if (data == null) return; - var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); - var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId); - if (_keepDefault || value != def) - MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value); + var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5)); + var identifier = new GmpIdentifier(metaFileInfo.PrimaryId); + MetaManipulations.TryAdd(identifier, value); } // Deserialize and check Est Entries and add them to the list if they are non-default. @@ -86,9 +80,7 @@ public partial class TexToolsMeta continue; var identifier = new EstIdentifier(id, type, gr); - var def = EstFile.GetDefault(_metaFileManager, type, gr, id); - if (_keepDefault || def != value) - MetaManipulations.TryAdd(identifier, value); + MetaManipulations.TryAdd(identifier, value); } } @@ -108,15 +100,10 @@ public partial class TexToolsMeta { var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId, metaFileInfo.EquipSlot, metaFileInfo.SecondaryType); - var file = new ImcFile(_metaFileManager, identifier); - var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0. foreach (var value in values) { identifier = identifier with { Variant = (Variant)i }; - var def = file.GetEntry(partIdx, (Variant)i); - if (_keepDefault || def != value && identifier.Validate()) - MetaManipulations.TryAdd(identifier, value); - + MetaManipulations.TryAdd(identifier, value); ++i; } } diff --git a/Penumbra/Import/TexToolsMeta.Rgsp.cs b/Penumbra/Import/TexToolsMeta.Rgsp.cs index 7b0bb5a8..77c70e6c 100644 --- a/Penumbra/Import/TexToolsMeta.Rgsp.cs +++ b/Penumbra/Import/TexToolsMeta.Rgsp.cs @@ -1,6 +1,5 @@ using Penumbra.GameData.Enums; using Penumbra.Meta; -using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; namespace Penumbra.Import; @@ -8,7 +7,7 @@ namespace Penumbra.Import; public partial class TexToolsMeta { // Parse a single rgsp file. - public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data, bool keepDefault) + public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data) { if (data.Length != 45 && data.Length != 42) { @@ -70,9 +69,7 @@ public partial class TexToolsMeta void Add(RspAttribute attribute, float value) { var identifier = new RspIdentifier(subRace, attribute); - var def = CmpFile.GetDefault(manager, subRace, attribute); - if (keepDefault || value != def.Value) - ret.MetaManipulations.TryAdd(identifier, new RspEntry(value)); + ret.MetaManipulations.TryAdd(identifier, new RspEntry(value)); } } } diff --git a/Penumbra/Import/TexToolsMeta.cs b/Penumbra/Import/TexToolsMeta.cs index c4a8e81f..f98eddbe 100644 --- a/Penumbra/Import/TexToolsMeta.cs +++ b/Penumbra/Import/TexToolsMeta.cs @@ -23,15 +23,11 @@ public partial class TexToolsMeta // The info class determines the files or table locations the changes need to apply to from the filename. public readonly uint Version; public readonly string FilePath; - public readonly MetaDictionary MetaManipulations = new(); - private readonly bool _keepDefault; + public readonly MetaDictionary MetaManipulations = new(); - private readonly MetaFileManager _metaFileManager; - public TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte[] data, bool keepDefault) + public TexToolsMeta(GamePathParser parser, byte[] data) { - _metaFileManager = metaFileManager; - _keepDefault = keepDefault; try { using var reader = new BinaryReader(new MemoryStream(data)); @@ -79,7 +75,6 @@ public partial class TexToolsMeta private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version) { - _metaFileManager = metaFileManager; FilePath = filePath; Version = version; } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index c5c8fb8b..050dab51 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -1,11 +1,15 @@ using System.Collections.Frozen; +using OtterGui.Classes; using OtterGui.Services; using Penumbra.Collections.Cache; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; +using Penumbra.Services; +using static Penumbra.GameData.Files.ShpkFile; namespace Penumbra.Mods.Editor; @@ -68,13 +72,157 @@ public class ModMetaEditor( Changes = false; } - public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) + public static bool DeleteDefaultValues(Mod mod, MetaFileManager metaFileManager, SaveService? saveService, bool deleteAll = false) + { + if (deleteAll) + { + var changes = false; + foreach (var container in mod.AllDataContainers) + { + if (!DeleteDefaultValues(metaFileManager, container.Manipulations)) + continue; + + saveService?.ImmediateSaveSync(new ModSaveGroup(container, metaFileManager.Config.ReplaceNonAsciiOnImport)); + changes = true; + } + + return changes; + } + + var defaultEntries = new MultiDictionary(); + var actualEntries = new HashSet(); + if (!FilterDefaultValues(mod.AllDataContainers, metaFileManager, defaultEntries, actualEntries)) + return false; + + var groups = new HashSet(); + DefaultSubMod? defaultMod = null; + foreach (var (defaultIdentifier, containers) in defaultEntries.Grouped) + { + if (!deleteAll && actualEntries.Contains(defaultIdentifier)) + continue; + + foreach (var container in containers) + { + if (!container.Manipulations.Remove(defaultIdentifier)) + continue; + + Penumbra.Log.Verbose($"Deleted default-valued meta-entry {defaultIdentifier}."); + if (container.Group is { } group) + groups.Add(group); + else if (container is DefaultSubMod d) + defaultMod = d; + } + } + + if (saveService is not null) + { + if (defaultMod is not null) + saveService.ImmediateSaveSync(new ModSaveGroup(defaultMod, metaFileManager.Config.ReplaceNonAsciiOnImport)); + foreach (var group in groups) + saveService.ImmediateSaveSync(new ModSaveGroup(group, metaFileManager.Config.ReplaceNonAsciiOnImport)); + } + + return defaultMod is not null || groups.Count > 0; + } + + public void DeleteDefaultValues() + => Changes = DeleteDefaultValues(metaFileManager, this); + + public void Apply(IModDataContainer container) + { + if (!Changes) + return; + + groupEditor.SetManipulations(container, this); + Changes = false; + } + + private static bool FilterDefaultValues(IEnumerable containers, MetaFileManager metaFileManager, + MultiDictionary defaultEntries, HashSet actualEntries) + { + if (!metaFileManager.CharacterUtility.Ready) + { + Penumbra.Log.Warning("Trying to filter default meta values before CharacterUtility was ready, skipped."); + return false; + } + + foreach (var container in containers) + { + foreach (var (key, value) in container.Manipulations.Imc) + { + var defaultEntry = ImcChecker.GetDefaultEntry(key, false); + if (defaultEntry.Entry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Eqp) + { + var defaultEntry = new EqpEntryInternal(ExpandedEqpFile.GetDefault(metaFileManager, key.SetId), key.Slot); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Eqdp) + { + var defaultEntry = new EqdpEntryInternal(ExpandedEqdpFile.GetDefault(metaFileManager, key), key.Slot); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Est) + { + var defaultEntry = EstFile.GetDefault(metaFileManager, key); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Gmp) + { + var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, key); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Rsp) + { + var defaultEntry = CmpFile.GetDefault(metaFileManager, key.SubRace, key.Attribute); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + + foreach (var (key, value) in container.Manipulations.Atch) + { + var defaultEntry = AtchCache.GetDefault(metaFileManager, key); + if (defaultEntry.Equals(value)) + defaultEntries.TryAdd(key, container); + else + actualEntries.Add(key); + } + } + + return true; + } + + private static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict) { if (!metaFileManager.CharacterUtility.Ready) { Penumbra.Log.Warning("Trying to delete default meta values before CharacterUtility was ready, skipped."); return false; } + var clone = dict.Clone(); dict.ClearForDefault(); @@ -189,16 +337,4 @@ public class ModMetaEditor( Penumbra.Log.Debug($"Deleted {count} default-valued meta-entries from a mod option."); return true; } - - public void DeleteDefaultValues() - => Changes = DeleteDefaultValues(metaFileManager, this); - - public void Apply(IModDataContainer container) - { - if (!Changes) - return; - - groupEditor.SetManipulations(container, this); - Changes = false; - } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 3e58c515..8b5b80d0 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using Penumbra.Api.Enums; +using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; @@ -82,9 +83,8 @@ public static partial class ModMigration foreach (var (gamePath, swapPath) in swaps) mod.Default.FileSwaps.Add(gamePath, swapPath); - creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true, true); - foreach (var group in mod.Groups) - saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); + creator.IncorporateAllMetaChanges(mod, true, true); + saveService.SaveAllOptionGroups(mod, false, creator.Config.ReplaceNonAsciiOnImport); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) @@ -182,7 +182,7 @@ public static partial class ModMigration Description = option.OptionDesc, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; } @@ -196,7 +196,7 @@ public static partial class ModMigration Priority = priority, }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - creator.IncorporateMetaChanges(subMod, mod.ModPath, false, true); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; } diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 0db83ef9..f4f182eb 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -80,14 +80,10 @@ public partial class ModCreator( LoadDefaultOption(mod); LoadAllGroups(mod); if (incorporateMetaChanges) - IncorporateAllMetaChanges(mod, true); - if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges) - foreach (var container in mod.AllDataContainers) - { - if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations)) - saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport)); - } - + IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges); + else if (deleteDefaultMetaChanges) + ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); + return true; } @@ -158,19 +154,21 @@ public partial class ModCreator( /// Convert all .meta and .rgsp files to their respective meta changes and add them to their options. /// Deletes the source files if delete is true. /// - public void IncorporateAllMetaChanges(Mod mod, bool delete) + public void IncorporateAllMetaChanges(Mod mod, bool delete, bool removeDefaultValues) { var changes = false; - List deleteList = new(); + List deleteList = []; foreach (var subMod in mod.AllDataContainers) { - var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false, true); + var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); changes |= localChanges; if (delete) deleteList.AddRange(localDeleteList); } DeleteDeleteList(deleteList, delete); + if (removeDefaultValues && !Config.KeepDefaultMetaChanges) + changes |= ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, null, false); if (!changes) return; @@ -184,8 +182,7 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, - bool deleteDefault) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) { var deleteList = new List(); var oldSize = option.Manipulations.Count; @@ -202,8 +199,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var meta = new TexToolsMeta(metaFileManager, gamePathParser, File.ReadAllBytes(file.FullName), - Config.KeepDefaultMetaChanges); + var meta = new TexToolsMeta(gamePathParser, File.ReadAllBytes(file.FullName)); Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); @@ -215,8 +211,7 @@ public partial class ModCreator( if (!file.Exists) continue; - var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName), - Config.KeepDefaultMetaChanges); + var rgsp = TexToolsMeta.FromRgspFile(metaFileManager, file.FullName, File.ReadAllBytes(file.FullName)); Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); @@ -232,9 +227,6 @@ public partial class ModCreator( DeleteDeleteList(deleteList, delete); var changes = oldSize < option.Manipulations.Count; - if (deleteDefault && !Config.KeepDefaultMetaChanges) - changes |= ModMetaEditor.DeleteDefaultValues(metaFileManager, option.Manipulations); - return (changes, deleteList); } @@ -289,7 +281,7 @@ public partial class ModCreator( foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); - IncorporateMetaChanges(mod, baseFolder, true, true); + IncorporateMetaChanges(mod, baseFolder, true); return mod; } @@ -308,7 +300,7 @@ public partial class ModCreator( mod.Default.Files.TryAdd(gamePath, file); } - IncorporateMetaChanges(mod.Default, directory, true, true); + IncorporateMetaChanges(mod.Default, directory, true); saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } From 129156a1c195f0cbce0c5bc464b4d6c8402d0ea1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Apr 2025 15:04:47 +0200 Subject: [PATCH 670/865] Add some more safety and better IPC for draw object storage. --- Penumbra.Api | 2 +- Penumbra/Api/Api/GameStateApi.cs | 28 ++++++++++- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/IpcProviders.cs | 2 + .../PathResolving/CollectionResolver.cs | 9 ++-- .../Interop/PathResolving/DrawObjectState.cs | 50 ++++++++++++------- Penumbra/Penumbra.cs | 3 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 38 +++++++------- 8 files changed, 89 insertions(+), 45 deletions(-) diff --git a/Penumbra.Api b/Penumbra.Api index bd56d828..47bd5424 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit bd56d82816b8366e19dddfb2dc7fd7f167e264ee +Subproject commit 47bd5424d04c667d0df1ac1dd1eeb3e50b476c2c diff --git a/Penumbra/Api/Api/GameStateApi.cs b/Penumbra/Api/Api/GameStateApi.cs index 7f70c6bf..74cde3a0 100644 --- a/Penumbra/Api/Api/GameStateApi.cs +++ b/Penumbra/Api/Api/GameStateApi.cs @@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable { private readonly CommunicatorService _communicator; private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; private readonly CutsceneService _cutsceneService; private readonly ResourceLoader _resourceLoader; public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService, - ResourceLoader resourceLoader) + ResourceLoader resourceLoader, DrawObjectState drawObjectState) { _communicator = communicator; _collectionResolver = collectionResolver; _cutsceneService = cutsceneService; _resourceLoader = resourceLoader; + _drawObjectState = drawObjectState; _resourceLoader.ResourceLoaded += OnResourceLoaded; _resourceLoader.PapRequested += OnPapRequested; _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); @@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable public int GetCutsceneParentIndex(int actorIdx) => _cutsceneService.GetParentIndex(actorIdx); + public Func GetCutsceneParentIndexFunc() + { + var weakRef = new WeakReference(_cutsceneService); + return idx => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed."); + + return c.GetParentIndex(idx); + }; + } + + public Func GetGameObjectFromDrawObjectFunc() + { + var weakRef = new WeakReference(_drawObjectState); + return model => + { + if (!weakRef.TryGetTarget(out var c)) + throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed."); + + return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero; + }; + } + public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx) => _cutsceneService.SetParentIndex(copyIdx, newParentIdx) ? PenumbraApiEc.Success diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 47d44cfc..38125627 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 = 8; + public const int FeatureVersion = 9; public void Dispose() { diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index d54faa6c..f5a6c16d 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -40,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState), IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState), IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState), + IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState), + IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState), IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta), IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta), diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index f14abbff..02e1be54 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -89,10 +89,13 @@ public sealed unsafe class CollectionResolver( /// Identify the correct collection for a draw object. public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache) { - var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject) + if (drawObject is null) + return DefaultCollection; + + Actor obj = drawObjectState.TryGetValue(drawObject, out var gameObject) ? gameObject.Item1 - : drawObjectState.LastGameObject); - return IdentifyCollection(obj, useCache); + : drawObjectState.LastGameObject; + return IdentifyCollection(obj.AsObject, useCache); } /// Get the default collection. diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index 28a0dd8d..6f3e457c 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -9,7 +9,7 @@ using Penumbra.Interop.Hooks.Objects; namespace Penumbra.Interop.PathResolving; -public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService +public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary, IService { private readonly ObjectManager _objects; private readonly CreateCharacterBase _createCharacterBase; @@ -18,7 +18,7 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject = []; + private readonly Dictionary _drawObjectToGameObject = []; public nint LastGameObject => _gameState.LastGameObject; @@ -41,11 +41,10 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject.ContainsKey(key); - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() => _drawObjectToGameObject.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() @@ -54,16 +53,28 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary _drawObjectToGameObject.Count; - public bool TryGetValue(nint drawObject, out (nint, bool) gameObject) - => _drawObjectToGameObject.TryGetValue(drawObject, out gameObject); + public bool TryGetValue(Model drawObject, out (Actor, ObjectIndex, bool) gameObject) + { + if (!_drawObjectToGameObject.TryGetValue(drawObject, out gameObject)) + return false; - public (nint, bool) this[nint key] + var currentObject = _objects[gameObject.Item2]; + if (currentObject != gameObject.Item1) + { + Penumbra.Log.Warning($"[DrawObjectState] Stored association {drawObject} -> {gameObject.Item1} has index {gameObject.Item2}, which resolves to {currentObject}."); + return false; + } + + return true; + } + + public (Actor, ObjectIndex, bool) this[Model key] => _drawObjectToGameObject[key]; - public IEnumerable Keys + public IEnumerable Keys => _drawObjectToGameObject.Keys; - public IEnumerable<(nint, bool)> Values + public IEnumerable<(Actor, ObjectIndex, bool)> Values => _drawObjectToGameObject.Values; public unsafe void Dispose() @@ -87,20 +98,21 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary 0x{(nint)a:X} (actual: 0x{pair.GameObject:X}, {pair.IsChild})."); + Penumbra.Log.Excessive( + $"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject.Address:X}, {pair.IsChild})."); } } @@ -119,9 +131,9 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary @@ -137,12 +149,12 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionaryChildObject, gameObject, true, true); if (!iterate) return; diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 79c7f2db..7f4c1b23 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -21,7 +21,6 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData.Data; -using Penumbra.GameData.Files; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; @@ -190,7 +189,7 @@ public class Penumbra : IDalamudPlugin ReadOnlySpan relevantPlugins = [ "Glamourer", "MareSynchronos", "CustomizePlus", "SimpleHeels", "VfxEditor", "heliosphere-plugin", "Ktisis", "Brio", "DynamicBridge", - "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", + "IllusioVitae", "Aetherment", "LoporritSync", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 6629c126..9dd18ddd 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -569,29 +569,31 @@ public class DebugTab : Window, ITab, IUiService { if (drawTree) { - using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit); + using var table = Table("###DrawObjectResolverTable", 8, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState - .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) - .ThenBy(kvp => kvp.Value.Item2) - .ThenBy(kvp => kvp.Key)) + foreach (var (drawObject, (gameObjectPtr, idx, child)) in _drawObjectState + .OrderBy(kvp => kvp.Value.Item2.Index) + .ThenBy(kvp => kvp.Value.Item3) + .ThenBy(kvp => kvp.Key.Address)) { - var gameObject = (GameObject*)gameObjectPtr; ImGui.TableNextColumn(); + ImUtf8.CopyOnClickSelectable($"{drawObject}"); + ImUtf8.DrawTableColumn($"{gameObjectPtr.Index}"); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, gameObjectPtr.Index != idx)) + { + ImUtf8.DrawTableColumn($"{idx}"); + } - ImGuiUtil.CopyOnClickSelectable($"0x{drawObject:X}"); + ImUtf8.DrawTableColumn(child ? "Child"u8 : "Main"u8); ImGui.TableNextColumn(); - ImGui.TextUnformatted(gameObject->ObjectIndex.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(child ? "Child" : "Main"); - ImGui.TableNextColumn(); - var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString()); - ImGuiUtil.CopyOnClickSelectable(address); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(name); - ImGui.TableNextColumn(); - var collection = _collectionResolver.IdentifyCollection(gameObject, true); - ImGui.TextUnformatted(collection.ModCollection.Identity.Name); + ImUtf8.CopyOnClickSelectable($"{gameObjectPtr}"); + using (ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF, _objects[idx] != gameObjectPtr)) + { + ImUtf8.DrawTableColumn($"{_objects[idx]}"); + } + ImUtf8.DrawTableColumn(gameObjectPtr.Utf8Name.Span); + var collection = _collectionResolver.IdentifyCollection(gameObjectPtr.AsObject, true); + ImUtf8.DrawTableColumn(collection.ModCollection.Identity.Name); } } } From 0ec6a17ac7c83c350f90fb6856c548f091b67f34 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:02:21 +0200 Subject: [PATCH 671/865] Add context to open backup directory. --- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1ccee2cb..1e6afa09 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -121,32 +121,42 @@ public class ModPanelEditTab( : backup.Exists ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." : $"Create exported archive of current mod at \"{backup.Name}\"."; - if (ImGuiUtil.DrawDisabledButton("Export Mod", buttonSize, tt, ModBackup.CreatingBackup)) + if (ImUtf8.ButtonEx("Export Mod"u8, tt, buttonSize, ModBackup.CreatingBackup)) backup.CreateAsync(); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImUtf8.OpenPopup("context"u8); + ImGui.SameLine(); tt = backup.Exists ? $"Delete existing mod export \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; - if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) + if (ImUtf8.ButtonEx("Delete Export"u8, tt, buttonSize, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Delete(); tt = backup.Exists ? $"Restore mod from exported file \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) + if (ImUtf8.ButtonEx("Restore From Export"u8, tt, buttonSize, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Restore(modManager); if (backup.Exists) { ImGui.SameLine(); - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) { - ImGui.TextUnformatted(FontAwesomeIcon.CheckCircle.ToIconString()); + ImUtf8.Text(FontAwesomeIcon.CheckCircle.ToIconString()); } - ImGuiUtil.HoverTooltip($"Export exists in \"{backup.Name}\"."); + ImUtf8.HoverTooltip($"Export exists in \"{backup.Name}\"."); } + + using var context = ImUtf8.Popup("context"u8); + if (!context) + return; + + if (ImUtf8.Selectable("Open Backup Directory"u8)) + Process.Start(new ProcessStartInfo(modExportManager.ExportDirectory.FullName) { UseShellExecute = true }); } /// Anything about editing the regular meta information about the mod. From dc336569ff6d79a9ab08cbf40670c74aed3065a5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:02:36 +0200 Subject: [PATCH 672/865] Add context to copy the full file path from redirections. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 6792c359..071b0551 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -210,6 +210,9 @@ public partial class ModEditWindow if (!context) return; + if (ImUtf8.Selectable("Copy Full File Path")) + ImUtf8.SetClipboardText(registry.File.FullName); + using (ImRaii.Disabled(registry.CurrentUsage == 0)) { if (ImUtf8.Selectable("Copy Game Paths"u8)) @@ -244,10 +247,8 @@ public partial class ModEditWindow using (ImRaii.Disabled(_cutPaths.Count == 0)) { if (ImUtf8.Selectable("Paste Game Paths"u8)) - { foreach (var path in _cutPaths) _editor.FileEditor.SetGamePath(_editor.Option!, i, -1, path); - } } } From f9b5a626cfa2da3b245cc5d982662a3fec76795a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:02:49 +0200 Subject: [PATCH 673/865] Add some migration stuff. --- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Services/MigrationManager.cs | 67 +---- Penumbra/Services/ModMigrator.cs | 331 +++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 9 +- Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs | 55 ++++ 6 files changed, 397 insertions(+), 69 deletions(-) create mode 100644 Penumbra/Services/ModMigrator.cs create mode 100644 Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs diff --git a/Penumbra.Api b/Penumbra.Api index 47bd5424..14652039 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 47bd5424d04c667d0df1ac1dd1eeb3e50b476c2c +Subproject commit 1465203967d08519c6716292bc5e5094c7fbcacc diff --git a/Penumbra.GameData b/Penumbra.GameData index 4769bbcd..e10d8f33 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 4769bbcdfce9e1d5a461c6b552b5b30ad6bc478e +Subproject commit e10d8f33a676ff4544d7ca05a93d555416f41222 diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index abc059e9..2438c0ad 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -1,76 +1,13 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui.Classes; using OtterGui.Services; -using Penumbra.Api.Enums; -using Penumbra.GameData.Files; -using Penumbra.Mods; -using Penumbra.String.Classes; using SharpCompress.Common; using SharpCompress.Readers; +using MdlFile = Penumbra.GameData.Files.MdlFile; +using MtrlFile = Penumbra.GameData.Files.MtrlFile; namespace Penumbra.Services; -public class ModMigrator -{ - private class FileData(string path) - { - public readonly string Path = path; - public readonly List<(string GamePath, int Option)> GamePaths = []; - } - - private sealed class FileDataDict : Dictionary - { - public void Add(string path, string gamePath, int option) - { - if (!TryGetValue(path, out var data)) - { - data = new FileData(path); - data.GamePaths.Add((gamePath, option)); - Add(path, data); - } - else - { - data.GamePaths.Add((gamePath, option)); - } - } - } - - private readonly FileDataDict Textures = []; - private readonly FileDataDict Models = []; - private readonly FileDataDict Materials = []; - - public void Update(IEnumerable mods) - { - CollectFiles(mods); - } - - private void CollectFiles(IEnumerable mods) - { - var option = 0; - foreach (var mod in mods) - { - AddDict(mod.Default.Files, option++); - foreach (var container in mod.Groups.SelectMany(group => group.DataContainers)) - AddDict(container.Files, option++); - } - - return; - - void AddDict(Dictionary dict, int currentOption) - { - foreach (var (gamePath, file) in dict) - { - switch (ResourceTypeExtensions.FromExtension(gamePath.Extension().Span)) - { - case ResourceType.Tex: Textures.Add(file.FullName, gamePath.ToString(), currentOption); break; - case ResourceType.Mdl: Models.Add(file.FullName, gamePath.ToString(), currentOption); break; - case ResourceType.Mtrl: Materials.Add(file.FullName, gamePath.ToString(), currentOption); break; - } - } - } - } -} - public class MigrationManager(Configuration config) : IService { public enum TaskType : byte diff --git a/Penumbra/Services/ModMigrator.cs b/Penumbra/Services/ModMigrator.cs new file mode 100644 index 00000000..20005c8f --- /dev/null +++ b/Penumbra/Services/ModMigrator.cs @@ -0,0 +1,331 @@ +using Dalamud.Plugin.Services; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Import.Textures; +using Penumbra.Mods; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Services; + +public class ModMigrator(IDataManager gameData, TextureManager textures) : IService +{ + private sealed class FileDataDict : MultiDictionary; + + private readonly Lazy _glassReferenceMaterial = new(() => + { + var bytes = gameData.GetFile("chara/equipment/e5001/material/v0001/mt_c0101e5001_met_b.mtrl"); + return new MtrlFile(bytes!.Data); + }); + + private readonly HashSet _changedMods = []; + private readonly HashSet _failedMods = []; + + private readonly FileDataDict Textures = []; + private readonly FileDataDict Models = []; + private readonly FileDataDict Materials = []; + private readonly FileDataDict FileSwaps = []; + + private readonly ConcurrentBag _messages = []; + + public void Update(IEnumerable mods) + { + CollectFiles(mods); + foreach (var (from, (to, container)) in FileSwaps) + MigrateFileSwaps(from, to, container); + foreach (var (model, list) in Models.Grouped) + MigrateModel(model, (Mod)list[0].Container.Mod); + } + + private void CollectFiles(IEnumerable mods) + { + foreach (var mod in mods) + { + foreach (var container in mod.AllDataContainers) + { + foreach (var (gamePath, file) in container.Files) + { + switch (ResourceTypeExtensions.FromExtension(gamePath.Extension().Span)) + { + case ResourceType.Tex: Textures.TryAdd(file.FullName, (gamePath.ToString(), container)); break; + case ResourceType.Mdl: Models.TryAdd(file.FullName, (gamePath.ToString(), container)); break; + case ResourceType.Mtrl: Materials.TryAdd(file.FullName, (gamePath.ToString(), container)); break; + } + } + + foreach (var (swapFrom, swapTo) in container.FileSwaps) + FileSwaps.TryAdd(swapTo.FullName, (swapFrom.ToString(), container)); + } + } + } + + public Task CreateIndexFile(string normalPath, string targetPath) + { + const int rowBlend = 17; + + return Task.Run(async () => + { + var tex = textures.LoadTex(normalPath); + var data = tex.GetPixelData(); + var rgbaData = new RgbaPixelData(data.Width, data.Height, data.Rgba); + if (!BitOperations.IsPow2(rgbaData.Height) || !BitOperations.IsPow2(rgbaData.Width)) + { + var requiredHeight = (int)BitOperations.RoundUpToPowerOf2((uint)rgbaData.Height); + var requiredWidth = (int)BitOperations.RoundUpToPowerOf2((uint)rgbaData.Width); + rgbaData = rgbaData.Resize((requiredWidth, requiredHeight)); + } + + Parallel.ForEach(Enumerable.Range(0, rgbaData.PixelData.Length / 4), idx => + { + var pixelIdx = 4 * idx; + var normal = rgbaData.PixelData[pixelIdx + 3]; + + // Copied from TT + var blendRem = normal % (2 * rowBlend); + var originalRow = normal / rowBlend; + switch (blendRem) + { + // Goes to next row, clamped to the closer row. + case > 25: + blendRem = 0; + ++originalRow; + break; + // Stays in this row, clamped to the closer row. + case > 17: blendRem = 17; break; + } + + var newBlend = (byte)(255 - MathF.Round(blendRem / 17f * 255f)); + + // Slight add here to push the color deeper into the row to ensure BC5 compression doesn't + // cause any artifacting. + var newRow = (byte)(originalRow / 2 * 17 + 4); + + rgbaData.PixelData[pixelIdx] = newRow; + rgbaData.PixelData[pixelIdx] = newBlend; + rgbaData.PixelData[pixelIdx] = 0; + rgbaData.PixelData[pixelIdx] = 255; + }); + await textures.SaveAs(CombinedTexture.TextureSaveType.BC5, true, true, new BaseImage(), targetPath, rgbaData.PixelData, + rgbaData.Width, rgbaData.Height); + }); + } + + private void MigrateModel(string filePath, Mod mod) + { + if (MigrationManager.TryMigrateSingleModel(filePath, true)) + { + _messages.Add($"Migrated model {filePath} in {mod.Name}."); + } + else + { + _messages.Add($"Failed to migrate model {filePath} in {mod.Name}"); + _failedMods.Add(mod); + } + } + + private void SetGlassReferenceValues(MtrlFile mtrl) + { + var reference = _glassReferenceMaterial.Value; + mtrl.ShaderPackage.ShaderKeys = reference.ShaderPackage.ShaderKeys.ToArray(); + mtrl.ShaderPackage.Constants = reference.ShaderPackage.Constants.ToArray(); + mtrl.AdditionalData = reference.AdditionalData.ToArray(); + mtrl.ShaderPackage.Flags &= ~(0x04u | 0x08u); + // From TT. + if (mtrl.Table is ColorTable t) + foreach (ref var row in t.AsRows()) + row.SpecularColor = new HalfColor((Half)0.8100586, (Half)0.8100586, (Half)0.8100586); + } + + private ref struct MaterialPack + { + public readonly MtrlFile File; + public readonly bool UsesMaskAsSpecular; + + private readonly Dictionary Samplers = []; + + public MaterialPack(MtrlFile file) + { + File = file; + UsesMaskAsSpecular = File.ShaderPackage.ShaderKeys.Any(x => x.Key is 0xC8BD1DEF && x.Value is 0xA02F4828 or 0x198D11CD); + Add(Samplers, TextureUsage.Normal, ShpkFile.NormalSamplerId); + Add(Samplers, TextureUsage.Index, ShpkFile.IndexSamplerId); + Add(Samplers, TextureUsage.Mask, ShpkFile.MaskSamplerId); + Add(Samplers, TextureUsage.Diffuse, ShpkFile.DiffuseSamplerId); + Add(Samplers, TextureUsage.Specular, ShpkFile.SpecularSamplerId); + return; + + void Add(Dictionary dict, TextureUsage usage, uint samplerId) + { + var idx = new SamplerIndex(file, samplerId); + if (idx.Texture >= 0) + dict.Add(usage, idx); + } + } + + public readonly record struct SamplerIndex(int Sampler, int Texture) + { + public SamplerIndex(MtrlFile file, uint samplerId) + : this(file.FindSampler(samplerId), -1) + => Texture = Sampler < 0 ? -1 : file.ShaderPackage.Samplers[Sampler].TextureIndex; + } + + public enum TextureUsage + { + Normal, + Index, + Mask, + Diffuse, + Specular, + } + + public static bool AdaptPath(IDataManager data, string path, TextureUsage usage, out string newPath) + { + newPath = path; + if (Path.GetExtension(newPath) is not ".tex") + return false; + + if (data.FileExists(newPath)) + return true; + + switch (usage) + { + case TextureUsage.Normal: + newPath = path.Replace("_n.tex", "_norm.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_n_", "_norm_"); + if (data.FileExists(newPath)) + return true; + + return false; + case TextureUsage.Index: return false; + case TextureUsage.Mask: + newPath = path.Replace("_m.tex", "_mult.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_m.tex", "_mask.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_m_", "_mult_"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_m_", "_mask_"); + if (data.FileExists(newPath)) + return true; + + return false; + case TextureUsage.Diffuse: + newPath = path.Replace("_d.tex", "_base.tex"); + if (data.FileExists(newPath)) + return true; + + newPath = path.Replace("_d_", "_base_"); + if (data.FileExists(newPath)) + return true; + + return false; + case TextureUsage.Specular: + return false; + default: throw new ArgumentOutOfRangeException(nameof(usage), usage, null); + } + } + } + + private void MigrateMaterial(string filePath, IReadOnlyList<(string GamePath, IModDataContainer Container)> redirections) + { + try + { + var bytes = File.ReadAllBytes(filePath); + var mtrl = new MtrlFile(bytes); + if (!CheckUpdateNeeded(mtrl)) + return; + + // Update colorsets, flags and character shader package. + var changes = mtrl.MigrateToDawntrail(); + + if (!changes) + switch (mtrl.ShaderPackage.Name) + { + case "hair.shpk": break; + case "characterglass.shpk": + SetGlassReferenceValues(mtrl); + changes = true; + break; + } + + // Remove DX11 flags and update paths if necessary. + foreach (ref var tex in mtrl.Textures.AsSpan()) + { + if (tex.DX11) + { + changes = true; + if (GamePaths.Tex.HandleDx11Path(tex, out var newPath)) + tex.Path = newPath; + tex.DX11 = false; + } + + if (gameData.FileExists(tex.Path)) + continue; + } + + // Dyeing, from TT. + if (mtrl.DyeTable is ColorDyeTable dye) + foreach (ref var row in dye.AsRows()) + row.Template += 1000; + } + catch + { + // ignored + } + + static bool CheckUpdateNeeded(MtrlFile mtrl) + { + if (!mtrl.IsDawntrail) + return true; + + if (mtrl.ShaderPackage.Name is not "hair.shpk") + return false; + + var foundOld = 0; + foreach (var c in mtrl.ShaderPackage.Constants) + { + switch (c.Id) + { + case 0x36080AD0: foundOld |= 1; break; // == 1, from TT + case 0x992869AB: foundOld |= 2; break; // == 3 (skin) or 4 (hair) from TT + } + + if (foundOld is 3) + return true; + } + + return false; + } + } + + private void MigrateFileSwaps(string swapFrom, string swapTo, IModDataContainer container) + { + var fromExists = gameData.FileExists(swapFrom); + var toExists = gameData.FileExists(swapTo); + if (fromExists && toExists) + return; + + if (ResourceTypeExtensions.FromExtension(Path.GetExtension(swapFrom.AsSpan())) is not ResourceType.Tex + || ResourceTypeExtensions.FromExtension(Path.GetExtension(swapTo.AsSpan())) is not ResourceType.Tex) + { + _messages.Add( + $"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}. Only textures may be migrated.{(fromExists ? "\n\tSource File does not exist." : "")}{(toExists ? "\n\tTarget File does not exist." : "")}"); + return; + } + + // try to migrate texture swaps + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 9dd18ddd..6d6222ec 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -106,6 +106,7 @@ public class DebugTab : Window, ITab, IUiService private readonly SchedulerResourceManagementService _schedulerService; private readonly ObjectIdentification _objectIdentification; private readonly RenderTargetDrawer _renderTargetDrawer; + private readonly ModMigratorDebug _modMigratorDebug; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -116,7 +117,8 @@ public class DebugTab : Window, ITab, IUiService TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer) + SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, + ModMigratorDebug modMigratorDebug) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -158,6 +160,7 @@ public class DebugTab : Window, ITab, IUiService _schedulerService = schedulerService; _objectIdentification = objectIdentification; _renderTargetDrawer = renderTargetDrawer; + _modMigratorDebug = modMigratorDebug; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -190,6 +193,7 @@ public class DebugTab : Window, ITab, IUiService DrawActorsDebug(); DrawCollectionCaches(); _texHeaderDrawer.Draw(); + _modMigratorDebug.Draw(); DrawShaderReplacementFixer(); DrawData(); DrawCrcCache(); @@ -591,6 +595,7 @@ public class DebugTab : Window, ITab, IUiService { ImUtf8.DrawTableColumn($"{_objects[idx]}"); } + ImUtf8.DrawTableColumn(gameObjectPtr.Utf8Name.Span); var collection = _collectionResolver.IdentifyCollection(gameObjectPtr.AsObject, true); ImUtf8.DrawTableColumn(collection.ModCollection.Identity.Name); @@ -751,7 +756,7 @@ public class DebugTab : Window, ITab, IUiService DrawChangedItemTest(); } - private string _changedItemPath = string.Empty; + private string _changedItemPath = string.Empty; private readonly Dictionary _changedItems = []; private void DrawChangedItemTest() diff --git a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs new file mode 100644 index 00000000..c8518315 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs @@ -0,0 +1,55 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Services; + +namespace Penumbra.UI.Tabs.Debug; + +public class ModMigratorDebug(ModMigrator migrator) : IUiService +{ + private string _inputPath = string.Empty; + private string _outputPath = string.Empty; + private Task? _indexTask; + private Task? _mdlTask; + + public void Draw() + { + if (!ImUtf8.CollapsingHeaderId("Mod Migrator"u8)) + return; + + ImUtf8.InputText("##input"u8, ref _inputPath, "Input Path..."u8); + ImUtf8.InputText("##output"u8, ref _outputPath, "Output Path..."u8); + + if (ImUtf8.ButtonEx("Create Index Texture"u8, "Requires input to be a path to a normal texture."u8, default, _inputPath.Length == 0 + || _outputPath.Length == 0 + || _indexTask is + { + IsCompleted: false, + })) + _indexTask = migrator.CreateIndexFile(_inputPath, _outputPath); + + if (_indexTask is not null) + { + ImGui.SameLine(); + ImUtf8.TextFrameAligned($"{_indexTask.Status}"); + } + + if (ImUtf8.ButtonEx("Update Model File"u8, "Requires input to be a path to a mdl."u8, default, _inputPath.Length == 0 + || _outputPath.Length == 0 + || _mdlTask is + { + IsCompleted: false, + })) + _mdlTask = Task.Run(() => + { + File.Copy(_inputPath, _outputPath, true); + MigrationManager.TryMigrateSingleModel(_outputPath, false); + }); + + if (_mdlTask is not null) + { + ImGui.SameLine(); + ImUtf8.TextFrameAligned($"{_mdlTask.Status}"); + } + } +} From f03a139e0e32acf0e35fff61cecd078f0ef72335 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 00:17:23 +0200 Subject: [PATCH 674/865] blech --- Penumbra/Services/ModMigrator.cs | 136 +++++++++++++++++-------------- 1 file changed, 77 insertions(+), 59 deletions(-) diff --git a/Penumbra/Services/ModMigrator.cs b/Penumbra/Services/ModMigrator.cs index 20005c8f..043d9631 100644 --- a/Penumbra/Services/ModMigrator.cs +++ b/Penumbra/Services/ModMigrator.cs @@ -1,17 +1,18 @@ -using Dalamud.Plugin.Services; -using OtterGui.Classes; -using OtterGui.Services; -using Penumbra.Api.Enums; -using Penumbra.GameData.Data; -using Penumbra.GameData.Files; -using Penumbra.GameData.Files.MaterialStructs; -using Penumbra.GameData.Structs; -using Penumbra.Import.Textures; -using Penumbra.Mods; -using Penumbra.Mods.SubMods; - -namespace Penumbra.Services; - +using Dalamud.Plugin.Services; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; +using Penumbra.GameData.Files.MaterialStructs; +using Penumbra.GameData.Structs; +using Penumbra.Import.Textures; +using Penumbra.Mods; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Services; + public class ModMigrator(IDataManager gameData, TextureManager textures) : IService { private sealed class FileDataDict : MultiDictionary; @@ -175,6 +176,7 @@ public class ModMigrator(IDataManager gameData, TextureManager textures) : IServ public enum TextureUsage { + Unknown, Normal, Index, Mask, @@ -191,51 +193,44 @@ public class ModMigrator(IDataManager gameData, TextureManager textures) : IServ if (data.FileExists(newPath)) return true; - switch (usage) + ReadOnlySpan<(string, string)> pairs = usage switch { - case TextureUsage.Normal: - newPath = path.Replace("_n.tex", "_norm.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_n_", "_norm_"); - if (data.FileExists(newPath)) - return true; - - return false; - case TextureUsage.Index: return false; - case TextureUsage.Mask: - newPath = path.Replace("_m.tex", "_mult.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_m.tex", "_mask.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_m_", "_mult_"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_m_", "_mask_"); - if (data.FileExists(newPath)) - return true; - - return false; - case TextureUsage.Diffuse: - newPath = path.Replace("_d.tex", "_base.tex"); - if (data.FileExists(newPath)) - return true; - - newPath = path.Replace("_d_", "_base_"); - if (data.FileExists(newPath)) - return true; - - return false; - case TextureUsage.Specular: - return false; - default: throw new ArgumentOutOfRangeException(nameof(usage), usage, null); + TextureUsage.Unknown => + [ + ("_n.tex", "_norm.tex"), + ("_m.tex", "_mult.tex"), + ("_m.tex", "_mask.tex"), + ("_d.tex", "_base.tex"), + ], + TextureUsage.Normal => + [ + ("_n_", "_norm_"), + ("_n.tex", "_norm.tex"), + ], + TextureUsage.Mask => + [ + ("_m_", "_mult_"), + ("_m_", "_mask_"), + ("_m.tex", "_mult.tex"), + ("_m.tex", "_mask.tex"), + ], + TextureUsage.Diffuse => + [ + ("_d_", "_base_"), + ("_d.tex", "_base.tex"), + ], + TextureUsage.Index => [], + TextureUsage.Specular => [], + _ => [], + }; + foreach (var (from, to) in pairs) + { + newPath = path.Replace(from, to); + if (data.FileExists(newPath)) + return true; } + + return false; } } @@ -326,6 +321,29 @@ public class ModMigrator(IDataManager gameData, TextureManager textures) : IServ return; } - // try to migrate texture swaps + var newSwapFrom = swapFrom; + if (!fromExists && !MaterialPack.AdaptPath(gameData, swapFrom, MaterialPack.TextureUsage.Unknown, out newSwapFrom)) + { + _messages.Add($"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}."); + return; + } + + var newSwapTo = swapTo; + if (!toExists && !MaterialPack.AdaptPath(gameData, swapTo, MaterialPack.TextureUsage.Unknown, out newSwapTo)) + { + _messages.Add($"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}."); + return; + } + + if (!Utf8GamePath.FromString(swapFrom, out var path) || !Utf8GamePath.FromString(newSwapFrom, out var newPath)) + { + _messages.Add( + $"Could not migrate file swap {swapFrom} -> {swapTo} in {container.Mod.Name}: {container.GetFullName()}. Unknown Error."); + return; + } + + container.FileSwaps.Remove(path); + container.FileSwaps.Add(newPath, new FullPath(newSwapTo)); + _changedMods.Add((Mod)container.Mod); } -} +} From 2bd0c895887a33c6161a743e40a0f063b5a9183f Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 10 Apr 2025 16:04:49 +0200 Subject: [PATCH 675/865] Better item sort for item swap selectors. --- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 52 ++++++++++++++--------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index cb56de08..f5d2a8c7 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -4,6 +4,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -15,6 +16,7 @@ using Penumbra.GameData.Structs; using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; @@ -46,19 +48,20 @@ public class ItemSwapTab : IDisposable, ITab, IUiService _config = config; _swapData = new ItemSwapContainer(metaFileManager, identifier); + var a = collectionManager.Active; _selectors = new Dictionary { // @formatter:off - [SwapType.Hat] = (new ItemSelector(itemService, selector, FullEquipType.Head), new ItemSelector(itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), - [SwapType.Top] = (new ItemSelector(itemService, selector, FullEquipType.Body), new ItemSelector(itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), - [SwapType.Gloves] = (new ItemSelector(itemService, selector, FullEquipType.Hands), new ItemSelector(itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), - [SwapType.Pants] = (new ItemSelector(itemService, selector, FullEquipType.Legs), new ItemSelector(itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), - [SwapType.Shoes] = (new ItemSelector(itemService, selector, FullEquipType.Feet), new ItemSelector(itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), - [SwapType.Earrings] = (new ItemSelector(itemService, selector, FullEquipType.Ears), new ItemSelector(itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), - [SwapType.Necklace] = (new ItemSelector(itemService, selector, FullEquipType.Neck), new ItemSelector(itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), - [SwapType.Bracelet] = (new ItemSelector(itemService, selector, FullEquipType.Wrists), new ItemSelector(itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), - [SwapType.Ring] = (new ItemSelector(itemService, selector, FullEquipType.Finger), new ItemSelector(itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), - [SwapType.Glasses] = (new ItemSelector(itemService, selector, FullEquipType.Glasses), new ItemSelector(itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), + [SwapType.Hat] = (new ItemSelector(a, itemService, selector, FullEquipType.Head), new ItemSelector(a, itemService, null, FullEquipType.Head), "Take this Hat", "and put it on this one" ), + [SwapType.Top] = (new ItemSelector(a, itemService, selector, FullEquipType.Body), new ItemSelector(a, itemService, null, FullEquipType.Body), "Take this Top", "and put it on this one" ), + [SwapType.Gloves] = (new ItemSelector(a, itemService, selector, FullEquipType.Hands), new ItemSelector(a, itemService, null, FullEquipType.Hands), "Take these Gloves", "and put them on these" ), + [SwapType.Pants] = (new ItemSelector(a, itemService, selector, FullEquipType.Legs), new ItemSelector(a, itemService, null, FullEquipType.Legs), "Take these Pants", "and put them on these" ), + [SwapType.Shoes] = (new ItemSelector(a, itemService, selector, FullEquipType.Feet), new ItemSelector(a, itemService, null, FullEquipType.Feet), "Take these Shoes", "and put them on these" ), + [SwapType.Earrings] = (new ItemSelector(a, itemService, selector, FullEquipType.Ears), new ItemSelector(a, itemService, null, FullEquipType.Ears), "Take these Earrings", "and put them on these" ), + [SwapType.Necklace] = (new ItemSelector(a, itemService, selector, FullEquipType.Neck), new ItemSelector(a, itemService, null, FullEquipType.Neck), "Take this Necklace", "and put it on this one" ), + [SwapType.Bracelet] = (new ItemSelector(a, itemService, selector, FullEquipType.Wrists), new ItemSelector(a, itemService, null, FullEquipType.Wrists), "Take these Bracelets", "and put them on these" ), + [SwapType.Ring] = (new ItemSelector(a, itemService, selector, FullEquipType.Finger), new ItemSelector(a, itemService, null, FullEquipType.Finger), "Take this Ring", "and put it on this one" ), + [SwapType.Glasses] = (new ItemSelector(a, itemService, selector, FullEquipType.Glasses), new ItemSelector(a, itemService, null, FullEquipType.Glasses), "Take these Glasses", "and put them on these" ), // @formatter:on }; @@ -134,23 +137,34 @@ public class ItemSwapTab : IDisposable, ITab, IUiService Glasses, } - private class ItemSelector(ItemData data, ModFileSystemSelector? selector, FullEquipType type) - : FilterComboCache<(EquipItem Item, bool InMod)>(() => + private class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type) + : FilterComboCache<(EquipItem Item, bool InMod, SingleArray InCollection)>(() => { var list = data.ByType[type]; - if (selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type)) - return list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name))).OrderByDescending(p => p.Item2).ToList(); - - return list.Select(i => (i, false)).ToList(); + var enumerable = selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type) + ? list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name), collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())) + .OrderByDescending(p => p.Item2).ThenByDescending(p => p.Item3.Count) + : selector is null + ? list.Select(i => (i, false, collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())).OrderBy(p => p.Item3.Count) + : list.Select(i => (i, false, collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())).OrderByDescending(p => p.Item3.Count); + return enumerable.ToList(); }, MouseWheelType.None, Penumbra.Log) { protected override bool DrawSelectable(int globalIdx, bool selected) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value(), Items[globalIdx].InMod); - return base.DrawSelectable(globalIdx, selected); + var (_, inMod, inCollection) = Items[globalIdx]; + using var color = inMod + ? ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeLocalPlayer.Value()) + : inCollection.Count > 0 + ? ImRaii.PushColor(ImGuiCol.Text, ColorId.ResTreeNonNetworked.Value()) + : null; + var ret = base.DrawSelectable(globalIdx, selected); + if (inCollection.Count > 0) + ImUtf8.HoverTooltip(string.Join('\n', inCollection.Select(m => m.Name.Text))); + return ret; } - protected override string ToString((EquipItem Item, bool InMod) obj) + protected override string ToString((EquipItem Item, bool InMod, SingleArray InCollection) obj) => obj.Item.Name; } From 5d5fc673b1ff703aec294cde7a557f7513de454c Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 10 Apr 2025 14:42:26 +0000 Subject: [PATCH 676/865] [CI] Updating repo.json for 1.3.6.8 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5163bb7d..8e88ad52 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.3.6.7", - "TestingAssemblyVersion": "1.3.6.7", + "AssemblyVersion": "1.3.6.8", + "TestingAssemblyVersion": "1.3.6.8", "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.3.6.7/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.7/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 0954f509127736e266dfc26d19593b42bb51c05a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:05:56 +0200 Subject: [PATCH 677/865] Update OtterGui, GameData, Namespaces. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 2 +- Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs | 1 + Penumbra/Api/IpcTester/TemporaryIpcTester.cs | 1 + Penumbra/Collections/Cache/CollectionCache.cs | 2 +- Penumbra/Collections/Manager/ActiveCollections.cs | 2 +- Penumbra/Collections/Manager/CollectionEditor.cs | 2 +- Penumbra/Collections/Manager/CollectionStorage.cs | 2 +- Penumbra/Collections/Manager/InheritanceManager.cs | 2 +- Penumbra/Collections/Manager/TempCollectionManager.cs | 2 +- Penumbra/Collections/ModCollectionIdentity.cs | 1 + Penumbra/Configuration.cs | 2 +- Penumbra/Import/Models/Export/MeshExporter.cs | 2 +- Penumbra/Import/Models/Import/MeshImporter.cs | 2 +- Penumbra/Import/Models/Import/ModelImporter.cs | 2 +- Penumbra/Import/Models/Import/PrimitiveImporter.cs | 2 +- Penumbra/Import/Models/Import/SubMeshImporter.cs | 2 +- Penumbra/Import/Models/ModelManager.cs | 2 +- Penumbra/Import/Models/SkeletonConverter.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 2 +- Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs | 2 +- Penumbra/Interop/PathResolving/CollectionResolver.cs | 2 +- Penumbra/Interop/ResourceTree/ResolveContext.cs | 2 +- Penumbra/Meta/Files/ImcFile.cs | 2 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 2 +- Penumbra/Mods/Editor/ModFileCollection.cs | 1 + Penumbra/Mods/Editor/ModMerger.cs | 1 + Penumbra/Mods/Editor/ModNormalizer.cs | 2 +- Penumbra/Mods/Editor/ModelMaterialInfo.cs | 2 +- Penumbra/Mods/Groups/CombiningModGroup.cs | 1 + Penumbra/Mods/Groups/ImcModGroup.cs | 2 +- Penumbra/Mods/Groups/MultiModGroup.cs | 2 +- Penumbra/Mods/Groups/SingleModGroup.cs | 2 +- Penumbra/Mods/Manager/ModMigration.cs | 2 +- Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs | 1 + Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs | 2 +- Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs | 2 +- Penumbra/Mods/Mod.cs | 1 + Penumbra/Mods/ModCreator.cs | 1 + Penumbra/Mods/Settings/ModSettings.cs | 2 +- Penumbra/Mods/SubMods/CombinedDataContainer.cs | 2 +- Penumbra/Mods/SubMods/OptionSubMod.cs | 2 +- Penumbra/Mods/SubMods/SubMod.cs | 2 +- Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs | 1 + Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs | 1 + Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs | 1 + Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs | 1 + Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 1 + Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 1 + Penumbra/UI/CollectionTab/CollectionCombo.cs | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 1 + Penumbra/UI/CollectionTab/InheritanceUi.cs | 1 + Penumbra/UI/FileDialogService.cs | 1 + Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs | 1 + Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 1 + Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs | 1 + Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs | 2 +- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 1 + Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- Penumbra/UI/ModsTab/MultiModPanel.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 1 + Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 2 +- 75 files changed, 75 insertions(+), 47 deletions(-) diff --git a/OtterGui b/OtterGui index 21ddfccb..5704b215 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 21ddfccb91ba3fa56e1c191e706ff91bffaa9515 +Subproject commit 5704b2151bcdbf18b04dff1b199ca2f35765504f diff --git a/Penumbra.GameData b/Penumbra.GameData index e10d8f33..62bbce59 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e10d8f33a676ff4544d7ca05a93d555416f41222 +Subproject commit 62bbce5981e961a91322ca1a7d3bb5be25f67185 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index fe9bf366..3ba17cf4 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; diff --git a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs index 088a77bd..48f3b4a8 100644 --- a/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs +++ b/Penumbra/Api/IpcTester/ResourceTreeIpcTester.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Utility; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index 3dc8862e..c106a867 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 42c8b27d..f6f038a1 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -1,5 +1,4 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods; @@ -8,6 +7,7 @@ using Penumbra.Mods.Editor; using Penumbra.String.Classes; using Penumbra.Util; using Penumbra.GameData.Data; +using OtterGui.Extensions; namespace Penumbra.Collections.Cache; diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 2ced8ad6..ffec7fd2 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -1,8 +1,8 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.GameData.Actors; diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index 5ccc38e2..f62eea3f 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Mods; diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index de723729..531b6333 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods; diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index 5e361bde..34582677 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; diff --git a/Penumbra/Collections/Manager/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs index 8aab5297..9476e38c 100644 --- a/Penumbra/Collections/Manager/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api; using Penumbra.Communication; diff --git a/Penumbra/Collections/ModCollectionIdentity.cs b/Penumbra/Collections/ModCollectionIdentity.cs index bd2d47c4..7050450c 100644 --- a/Penumbra/Collections/ModCollectionIdentity.cs +++ b/Penumbra/Collections/ModCollectionIdentity.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Extensions; using Penumbra.Collections.Manager; namespace Penumbra.Collections; diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 3a9bcdc4..bd6ccfb1 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,8 +1,8 @@ using Dalamud.Configuration; using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/Import/Models/Export/MeshExporter.cs b/Penumbra/Import/Models/Export/MeshExporter.cs index 32b9b323..0070a808 100644 --- a/Penumbra/Import/Models/Export/MeshExporter.cs +++ b/Penumbra/Import/Models/Export/MeshExporter.cs @@ -2,7 +2,7 @@ using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Nodes; using Lumina.Extensions; -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Geometry; diff --git a/Penumbra/Import/Models/Import/MeshImporter.cs b/Penumbra/Import/Models/Import/MeshImporter.cs index 6a46fb9f..16fe2ca0 100644 --- a/Penumbra/Import/Models/Import/MeshImporter.cs +++ b/Penumbra/Import/Models/Import/MeshImporter.cs @@ -1,5 +1,5 @@ using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; diff --git a/Penumbra/Import/Models/Import/ModelImporter.cs b/Penumbra/Import/Models/Import/ModelImporter.cs index 502d060a..f4eefccc 100644 --- a/Penumbra/Import/Models/Import/ModelImporter.cs +++ b/Penumbra/Import/Models/Import/ModelImporter.cs @@ -1,5 +1,5 @@ using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ModelStructs; using SharpGLTF.Schema2; diff --git a/Penumbra/Import/Models/Import/PrimitiveImporter.cs b/Penumbra/Import/Models/Import/PrimitiveImporter.cs index 5df7597e..57c7929f 100644 --- a/Penumbra/Import/Models/Import/PrimitiveImporter.cs +++ b/Penumbra/Import/Models/Import/PrimitiveImporter.cs @@ -1,5 +1,5 @@ using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; diff --git a/Penumbra/Import/Models/Import/SubMeshImporter.cs b/Penumbra/Import/Models/Import/SubMeshImporter.cs index df08eea3..6aa46fb6 100644 --- a/Penumbra/Import/Models/Import/SubMeshImporter.cs +++ b/Penumbra/Import/Models/Import/SubMeshImporter.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using SharpGLTF.Schema2; namespace Penumbra.Import.Models.Import; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 19d06a52..6818ad64 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; using Lumina.Data.Parsing; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Tasks; using Penumbra.Collections.Manager; diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index 25e74332..e180662d 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -1,5 +1,5 @@ using System.Xml; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Import.Models.Export; namespace Penumbra.Import.Models; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 7bbb762e..1c28aef2 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index 5fdec816..caf43d08 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,7 +1,7 @@ using System.Text.Unicode; using Dalamud.Hooking; using Iced.Intel; -using OtterGui; +using OtterGui.Extensions; using Penumbra.String.Classes; using Swan; diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 02e1be54..10795e6d 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -1,7 +1,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index 99360077..013d7db7 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Text.HelperObjects; using Penumbra.Api.Enums; using Penumbra.Collections; diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index 0a0faf1e..b8db66dd 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index 2a23ffad..da580794 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 7667910f..15bd179e 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index e3eb5f54..88941edf 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Utility; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 3e367a3b..527dbf7c 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.ImGuiNotification; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Tasks; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/Editor/ModelMaterialInfo.cs b/Penumbra/Mods/Editor/ModelMaterialInfo.cs index 741c2388..fe46048f 100644 --- a/Penumbra/Mods/Editor/ModelMaterialInfo.cs +++ b/Penumbra/Mods/Editor/ModelMaterialInfo.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Compression; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 90a962b7..d3f14101 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 5ec32274..34174f7f 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -1,8 +1,8 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 82555314..558ee6be 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -1,8 +1,8 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index c250182a..f376c1c9 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 8b5b80d0..f3b25f1a 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs index ce5db454..5acf5eb5 100644 --- a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs index a7b73ac9..12ed4c60 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; using Penumbra.Mods.SubMods; diff --git a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs index c067102e..5c5ed4f1 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs @@ -1,5 +1,5 @@ -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index efd92631..99f86517 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,5 +1,6 @@ using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.GameData.Data; using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index f4f182eb..df476a6f 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Api.Enums; diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 0420ee86..07217d4d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Mods.Editor; diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs index 2c410c1c..b467c360 100644 --- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs +++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs @@ -1,5 +1,5 @@ using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index 8fac52d8..9044350d 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index fcb6cc0e..a7a2ee61 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs index 176ec3f4..f413a6a2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Constants.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget.Editors; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs index b76cffc2..ee5341b2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ShaderPackage.cs @@ -4,6 +4,7 @@ using ImGuiNET; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.GameData; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs index 7ecd97e0..ac88f77c 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.Textures.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 80b10607..5bc70fc3 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -2,7 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using Newtonsoft.Json.Linq; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 258e51ff..36154105 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Text; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 071b0551..3f63967e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Editor; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index 59b38465..4c946fe7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.UI.AdvancedWindow.Materials; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b436448f..fc197bc0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,4 +1,4 @@ -using OtterGui; +using OtterGui.Extensions; using Penumbra.GameData; using Penumbra.GameData.Files; using Penumbra.Import.Models; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 8fbe5a68..0c8c496f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -3,6 +3,7 @@ using ImGuiNET; using Lumina.Data.Parsing; using OtterGui; using OtterGui.Custom; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs index 41f1da26..a6a75e0d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShaderPackages.cs @@ -12,6 +12,7 @@ using static Penumbra.GameData.Files.ShpkFile; using OtterGui.Widgets; using OtterGui.Text; using Penumbra.GameData.Structs; +using OtterGui.Extensions; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index b5b39e90..6c2953e0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -1,7 +1,7 @@ using Dalamud.Utility; using Newtonsoft.Json.Linq; -using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using Penumbra.GameData.Files; using Penumbra.GameData.Files.ShaderStructs; using Penumbra.GameData.Interop; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 4664372e..ee4e1eda 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -1,5 +1,6 @@ using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterTex; using Penumbra.Import.Textures; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 7f1a8ac5..ccbbf0db 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -6,6 +6,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index bd62089f..3c110fab 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index eb9aa93d..4d33a3fc 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -9,6 +9,7 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.String; +using OtterGui.Extensions; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/CollectionTab/CollectionCombo.cs b/Penumbra/UI/CollectionTab/CollectionCombo.cs index 0259713f..98dc924f 100644 --- a/Penumbra/UI/CollectionTab/CollectionCombo.cs +++ b/Penumbra/UI/CollectionTab/CollectionCombo.cs @@ -1,5 +1,5 @@ using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index 8b41b105..dc0e71b5 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -9,6 +9,7 @@ using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.Collections.Manager; diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index ce3cc3cb..cdc1e83e 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Collections; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index cc2a7f6a..6773bc88 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Services; diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index f32e6da6..5bd5dfdf 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index 786bb8ff..3d330093 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Text.Widget; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 8dee13bf..666fce61 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.Components; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index 89812346..e9ab72ae 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.ImGuiNotification; using ImGuiNET; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs index f0275853..04ca6c82 100644 --- a/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/MultiModGroupEditDrawer.cs @@ -1,5 +1,5 @@ using Dalamud.Interface; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index be2dbd73..492a8fb7 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -1,6 +1,6 @@ using Dalamud.Interface; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Mods.Groups; diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 2b4b665b..ec020c86 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -1,6 +1,6 @@ using Dalamud.Interface.Utility; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index f7b6b42d..c750b8b0 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -2,6 +2,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 38340a2d..3988de35 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using OtterGui; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; @@ -11,6 +10,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Mods.Settings; using Penumbra.UI.ModsTab.Groups; +using OtterGui.Extensions; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 0e9b5d39..ff5f636d 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -1,7 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility; using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 8de613d4..12355672 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Services; using Penumbra.Mods.Manager; diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index d9058083..eb9f05d9 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,5 +1,5 @@ using ImGuiNET; -using OtterGui; +using OtterGui.Extensions; using OtterGui.Text; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; From 53ef42adfa109cd073783acd328d096e01634453 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:06:09 +0200 Subject: [PATCH 678/865] Update EST Customization identification. --- Penumbra/Meta/Manipulations/Est.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 05d4c014..46a275a5 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -33,7 +33,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende var (gender, race) = GenderRace.Split(); var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {gender.ToName()} {race.ToName()} Hair {SetId}", () => IdentifiedCustomization.Hair(race, gender, id)); + $"Customization: {race.ToName()} {gender.ToName()} Hair {SetId}", () => IdentifiedCustomization.Hair(race, gender, id)); break; } case EstType.Face: @@ -41,7 +41,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende var (gender, race) = GenderRace.Split(); var id = (CustomizeValue)SetId.Id; changedItems.UpdateCountOrSet( - $"Customization: {gender.ToName()} {race.ToName()} Face {SetId}", + $"Customization: {race.ToName()} {gender.ToName()} Face {SetId}", () => IdentifiedCustomization.Face(race, gender, id)); break; } From 0c768979d4e34969aabbb4cf1dd4dc90cc1ed8e4 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:06:22 +0200 Subject: [PATCH 679/865] Don't use DalamudPackager for no reason. --- Penumbra/Penumbra.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f93b1815..f668f775 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -11,6 +11,7 @@ PROFILING; + false From cbebfe5e99709b7babc3de2245eaadb2dd48c763 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 17 Apr 2025 01:06:58 +0200 Subject: [PATCH 680/865] Fix sizing of mod panel. --- Penumbra/UI/Tabs/ModsTab.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 8b4913c8..b77240ec 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -55,12 +55,12 @@ public class ModsTab( { selector.Draw(GetModSelectorSize(config)); ImGui.SameLine(); + ImGui.SetCursorPosX(MathF.Round(ImGui.GetCursorPosX())); using var group = ImRaii.Group(); collectionHeader.Draw(false); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), + using (var child = ImRaii.Child("##ModsTabMod", new Vector2(ImGui.GetContentRegionAvail().X, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), true, ImGuiWindowFlags.HorizontalScrollbar)) { style.Pop(); @@ -94,9 +94,9 @@ public class ModsTab( var relativeSize = config.ScaleModSelector ? Math.Clamp(config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) : 0; - return !config.ScaleModSelector - ? absoluteSize - : Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100); + return MathF.Round(config.ScaleModSelector + ? Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100) + : absoluteSize); } private void DrawRedrawLine() From a5d221dc1359281e68614f5fb9a3db060a839278 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 18 Apr 2025 00:14:11 +0200 Subject: [PATCH 681/865] Make temporary mode checkbox more visible. --- OtterGui | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 31 +++++++++---------- Penumbra/UI/Tabs/Debug/DebugTab.cs | 1 + 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/OtterGui b/OtterGui index 5704b215..ac32553b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5704b2151bcdbf18b04dff1b199ca2f35765504f +Subproject commit ac32553b1e2e9feca7b9cd0c1b16eae81d5fcc31 diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index d7a81876..aa492362 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -1,10 +1,9 @@ using Dalamud.Interface; using ImGuiNET; -using OtterGui.Raii; using OtterGui; +using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; -using OtterGui.Text.Widget; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -15,13 +14,12 @@ namespace Penumbra.UI.Classes; public class CollectionSelectHeader : IUiService { - private readonly CollectionCombo _collectionCombo; - private readonly ActiveCollections _activeCollections; - private readonly TutorialService _tutorial; - private readonly ModSelection _selection; - private readonly CollectionResolver _resolver; - private readonly FontAwesomeCheckbox _temporaryCheckbox = new(FontAwesomeIcon.Stopwatch); - private readonly Configuration _config; + private readonly CollectionCombo _collectionCombo; + private readonly ActiveCollections _activeCollections; + private readonly TutorialService _tutorial; + private readonly ModSelection _selection; + private readonly CollectionResolver _resolver; + private readonly Configuration _config; public CollectionSelectHeader(CollectionManager collectionManager, TutorialService tutorial, ModSelection selection, CollectionResolver resolver, Configuration config) @@ -64,14 +62,15 @@ public class CollectionSelectHeader : IUiService var hold = _config.IncognitoModifier.IsActive(); using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale)) { - var tint = ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint); - using var color = ImRaii.PushColor(ImGuiCol.FrameBgHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) - .Push(ImGuiCol.FrameBgActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) - .Push(ImGuiCol.CheckMark, tint) - .Push(ImGuiCol.Border, tint, _config.DefaultTemporaryMode); - if (_temporaryCheckbox.Draw("##tempCheck"u8, _config.DefaultTemporaryMode, out var newValue) && hold) + var tint = _config.DefaultTemporaryMode + ? ImGuiCol.Text.Tinted(ColorId.TemporaryModSettingsTint) + : ImGui.GetColorU32(ImGuiCol.TextDisabled); + using var color = ImRaii.PushColor(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.ButtonActive, ImGui.GetColorU32(ImGuiCol.FrameBg), !hold) + .Push(ImGuiCol.Border, tint, _config.DefaultTemporaryMode); + if (ImUtf8.IconButton(FontAwesomeIcon.Stopwatch, ""u8, default, false, tint, ImGui.GetColorU32(ImGuiCol.FrameBg)) && hold) { - _config.DefaultTemporaryMode = newValue; + _config.DefaultTemporaryMode = !_config.DefaultTemporaryMode; _config.Save(); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 6d6222ec..b7bc8edf 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -12,6 +12,7 @@ using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; From 117724b0aebc0b063a5edb81342b28cca44a4fc7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 19 Apr 2025 23:11:45 +0200 Subject: [PATCH 682/865] Update npc names. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Services/StaticServiceManager.cs | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index ac32553b..089ed82a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit ac32553b1e2e9feca7b9cd0c1b16eae81d5fcc31 +Subproject commit 089ed82a53dc75b0d3be469d2a005e6096c4b2d2 diff --git a/Penumbra.GameData b/Penumbra.GameData index 62bbce59..002260d9 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 62bbce5981e961a91322ca1a7d3bb5be25f67185 +Subproject commit 002260d9815e571f1496c50374f5b712818e9880 diff --git a/Penumbra.String b/Penumbra.String index 2896c056..0e5dcd1a 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 2896c0561f60827f97408650d52a15c38f4d9d10 +Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5 diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index c0dc9314..27582395 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -17,6 +17,8 @@ using IPenumbraApi = Penumbra.Api.Api.IPenumbraApi; namespace Penumbra.Services; +#pragma warning disable SeStringEvaluator + public static class StaticServiceManager { public static ServiceManager CreateProvider(Penumbra penumbra, IDalamudPluginInterface pi, Logger log) @@ -60,5 +62,6 @@ public static class StaticServiceManager .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) - .AddDalamudService(pi); + .AddDalamudService(pi) + .AddDalamudService(pi); } From 363d115be8980009db8bccc87204b12ce1d24e14 Mon Sep 17 00:00:00 2001 From: Caraxi Date: Sun, 20 Apr 2025 00:35:36 +0930 Subject: [PATCH 683/865] Add filter for temporary mods --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 14 ++++++++++++++ Penumbra/UI/ModsTab/ModFilter.cs | 7 +++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index a0383329..6586747c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -647,6 +647,20 @@ public sealed class ModFileSystemSelector : FileSystemSelector TriStatePairs = [ @@ -38,6 +40,7 @@ public static class ModFilterExtensions (ModFilter.HasFiles, ModFilter.HasNoFiles, "Has Redirections"), (ModFilter.HasMetaManipulations, ModFilter.HasNoMetaManipulations, "Has Meta Manipulations"), (ModFilter.HasFileSwaps, ModFilter.HasNoFileSwaps, "Has File Swaps"), + (ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"), ]; public static IReadOnlyList> Groups = From 0fe4a3671ab6e577ca39a2d94bd714df1a2c3a06 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 8 May 2025 23:46:25 +0200 Subject: [PATCH 684/865] Improve small issue with redraw service. --- Penumbra/Interop/Services/RedrawService.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index 8f20ca5e..08e9ddf5 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -10,7 +10,6 @@ using OtterGui.Services; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Communication; -using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.Structs; @@ -354,21 +353,14 @@ public sealed unsafe partial class RedrawService : IDisposable { switch (settings) { - case RedrawType.Redraw: - ReloadActor(actor); - break; - case RedrawType.AfterGPose: - ReloadActorAfterGPose(actor); - break; - default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null); + case RedrawType.Redraw: ReloadActor(actor); break; + case RedrawType.AfterGPose: ReloadActorAfterGPose(actor); break; + default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null); } } private IGameObject? GetLocalPlayer() - { - var gPosePlayer = _objects.GetDalamudObject(GPosePlayerIdx); - return gPosePlayer ?? _objects.GetDalamudObject(0); - } + => InGPose ? _objects.GetDalamudObject(GPosePlayerIdx) ?? _objects.GetDalamudObject(0) : _objects.GetDalamudObject(0); public bool GetName(string lowerName, out IGameObject? actor) { From 0adec35848f8e02838d7d43e755cf6e0f8444980 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 00:26:59 +0200 Subject: [PATCH 685/865] Add initial support for custom shapes. --- OtterGui | 2 +- Penumbra.GameData | 2 +- .../Communication/ModelAttributeComputed.cs | 24 ++++ Penumbra/Configuration.cs | 4 +- .../Hooks/PostProcessing/AttributeHooks.cs | 110 +++++++++++++++ .../Hooks/Resources/ResolvePathHooksBase.cs | 22 --- Penumbra/Meta/ShapeManager.cs | 127 ++++++++++++++++++ Penumbra/Meta/ShapeString.cs | 89 ++++++++++++ Penumbra/Penumbra.cs | 7 +- Penumbra/Services/CommunicatorService.cs | 4 + Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 12 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 89 +++++++----- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 71 ++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 8 +- Penumbra/packages.lock.json | 6 - 15 files changed, 502 insertions(+), 75 deletions(-) create mode 100644 Penumbra/Communication/ModelAttributeComputed.cs create mode 100644 Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs create mode 100644 Penumbra/Meta/ShapeManager.cs create mode 100644 Penumbra/Meta/ShapeString.cs create mode 100644 Penumbra/UI/Tabs/Debug/ShapeInspector.cs diff --git a/OtterGui b/OtterGui index 089ed82a..86b49242 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 089ed82a53dc75b0d3be469d2a005e6096c4b2d2 +Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c diff --git a/Penumbra.GameData b/Penumbra.GameData index 002260d9..0ca50105 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 002260d9815e571f1496c50374f5b712818e9880 +Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6 diff --git a/Penumbra/Communication/ModelAttributeComputed.cs b/Penumbra/Communication/ModelAttributeComputed.cs new file mode 100644 index 00000000..389f56b6 --- /dev/null +++ b/Penumbra/Communication/ModelAttributeComputed.cs @@ -0,0 +1,24 @@ +using OtterGui.Classes; +using Penumbra.Collections; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever a model recomputes its attribute masks. +/// +/// Parameter is the game object that recomputed its attributes. +/// Parameter is the draw object on which the recomputation was called. +/// Parameter is the collection associated with the game object. +/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. +/// +public sealed class ModelAttributeComputed() + : EventWrapper(nameof(ModelAttributeComputed)) +{ + public enum Priority + { + /// + ShapeManager = 0, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index bd6ccfb1..8c50dad7 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -67,6 +67,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 EnableCustomShapes { get; set; } = true; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed; public int OptionGroupCollapsibleMin { get; set; } = 5; @@ -84,9 +85,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService [JsonProperty(Order = int.MaxValue)] public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; public string DefaultImportFolder { get; set; } = string.Empty; diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs new file mode 100644 index 00000000..861962ee --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs @@ -0,0 +1,110 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Services; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +public sealed unsafe class AttributeHooks : IRequiredService, IDisposable +{ + private delegate void SetupAttributes(Human* human, byte* data); + private delegate void AttributeUpdate(Human* human); + + private readonly Configuration _config; + private readonly ModelAttributeComputed _event; + private readonly CollectionResolver _resolver; + + private readonly AttributeHook[] _hooks; + private readonly Task> _updateHook; + private ModCollection _identifiedCollection = ModCollection.Empty; + private Actor _identifiedActor = Actor.Null; + private bool _inUpdate; + + public AttributeHooks(Configuration config, CommunicatorService communication, CollectionResolver resolver, HookManager hooks) + { + _config = config; + _event = communication.ModelAttributeComputed; + _resolver = resolver; + _hooks = + [ + new AttributeHook(this, hooks, Sigs.SetupTopModelAttributes, _config.EnableCustomShapes, HumanSlot.Body), + new AttributeHook(this, hooks, Sigs.SetupHandModelAttributes, _config.EnableCustomShapes, HumanSlot.Hands), + new AttributeHook(this, hooks, Sigs.SetupLegModelAttributes, _config.EnableCustomShapes, HumanSlot.Legs), + new AttributeHook(this, hooks, Sigs.SetupFootModelAttributes, _config.EnableCustomShapes, HumanSlot.Feet), + ]; + _updateHook = hooks.CreateHook("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour, + _config.EnableCustomShapes); + } + + private class AttributeHook + { + private readonly AttributeHooks _parent; + public readonly string Name; + public readonly Task> Hook; + public readonly uint ModelIndex; + public readonly HumanSlot Slot; + + public AttributeHook(AttributeHooks parent, HookManager hooks, string signature, bool enabled, HumanSlot slot) + { + _parent = parent; + Name = $"Setup{slot}Attributes"; + Slot = slot; + ModelIndex = slot.ToIndex(); + Hook = hooks.CreateHook(Name, signature, Detour, enabled); + } + + private void Detour(Human* human, byte* data) + { + Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{_parent._identifiedActor.Address:X})."); + Hook.Result.Original(human, data); + if (_parent is { _inUpdate: true, _identifiedActor.Valid: true }) + _parent._event.Invoke(_parent._identifiedActor, human, _parent._identifiedCollection, Slot); + } + } + + private void UpdateAttributesDetour(Human* human) + { + var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); + _identifiedActor = resolveData.AssociatedGameObject; + _identifiedCollection = resolveData.ModCollection; + _inUpdate = true; + Penumbra.Log.Excessive($"[UpdateAttributes] Invoked on 0x{(ulong)human:X} (0x{_identifiedActor.Address:X})."); + _event.Invoke(_identifiedActor, human, _identifiedCollection, HumanSlot.Unknown); + _updateHook.Result.Original(human); + _inUpdate = false; + } + + public void SetState(bool enabled) + { + if (_config.EnableCustomShapes == enabled) + return; + + _config.EnableCustomShapes = enabled; + _config.Save(); + if (enabled) + { + foreach (var hook in _hooks) + hook.Hook.Result.Enable(); + _updateHook.Result.Enable(); + } + else + { + foreach (var hook in _hooks) + hook.Hook.Result.Disable(); + _updateHook.Result.Disable(); + } + } + + public void Dispose() + { + foreach (var hook in _hooks) + hook.Hook.Result.Dispose(); + _updateHook.Result.Dispose(); + } +} diff --git a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs index 54066782..8a45ec2c 100644 --- a/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs +++ b/Penumbra/Interop/Hooks/Resources/ResolvePathHooksBase.cs @@ -246,28 +246,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable return ret; } - [StructLayout(LayoutKind.Explicit)] - private struct ChangedEquipData - { - [FieldOffset(0)] - public PrimaryId Model; - - [FieldOffset(2)] - public Variant Variant; - - [FieldOffset(8)] - public PrimaryId BonusModel; - - [FieldOffset(10)] - public Variant BonusVariant; - - [FieldOffset(20)] - public ushort VfxId; - - [FieldOffset(22)] - public GenderRace GenderRace; - } - private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam) { switch (slotIndex) diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs new file mode 100644 index 00000000..4356086a --- /dev/null +++ b/Penumbra/Meta/ShapeManager.cs @@ -0,0 +1,127 @@ +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Communication; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.Services; + +namespace Penumbra.Meta; + +public class ShapeManager : IRequiredService, IDisposable +{ + public const int NumSlots = 4; + private readonly CommunicatorService _communicator; + + private static ReadOnlySpan UsedModels + => [1, 2, 3, 4]; + + public ShapeManager(CommunicatorService communicator) + { + _communicator = communicator; + _communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager); + } + + private readonly Dictionary[] _temporaryIndices = + Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); + + private readonly uint[] _temporaryMasks = new uint[NumSlots]; + private readonly uint[] _temporaryValues = new uint[NumSlots]; + + private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection, HumanSlot slot) + { + int index; + switch (slot) + { + case HumanSlot.Unknown: + ResetCache(model); + return; + case HumanSlot.Body: index = 0; break; + case HumanSlot.Hands: index = 1; break; + case HumanSlot.Legs: index = 2; break; + case HumanSlot.Feet: index = 3; break; + default: return; + } + + if (_temporaryMasks[index] is 0) + return; + + var modelIndex = UsedModels[index]; + var currentMask = model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask; + var newMask = (currentMask & ~_temporaryMasks[index]) | _temporaryValues[index]; + Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); + model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask = newMask; + } + + public void Dispose() + { + _communicator.ModelAttributeComputed.Unsubscribe(OnAttributeComputed); + } + + private unsafe void ResetCache(Model human) + { + for (var i = 0; i < NumSlots; ++i) + { + _temporaryMasks[i] = 0; + _temporaryValues[i] = 0; + _temporaryIndices[i].Clear(); + + var modelIndex = UsedModels[i]; + var model = human.AsHuman->Models[modelIndex]; + if (model is null || model->ModelResourceHandle is null) + continue; + + ref var shapes = ref model->ModelResourceHandle->Shapes; + foreach (var (shape, index) in shapes.Where(kvp => CheckShapes(kvp.Key.AsSpan(), modelIndex))) + { + if (ShapeString.TryRead(shape.Value, out var shapeString)) + { + _temporaryIndices[i].TryAdd(shapeString, index); + _temporaryMasks[i] |= (ushort)(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); + } + } + } + + UpdateMasks(); + } + + private static bool CheckShapes(ReadOnlySpan shape, byte index) + => index switch + { + 1 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_wr_"u8), + 2 => shape.StartsWith("shp_wr_"u8), + 3 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_an"u8), + 4 => shape.StartsWith("shp_an"u8), + _ => false, + }; + + private void UpdateMasks() + { + foreach (var (shape, topIndex) in _temporaryIndices[0]) + { + if (_temporaryIndices[1].TryGetValue(shape, out var handIndex)) + { + _temporaryValues[0] |= 1u << topIndex; + _temporaryValues[1] |= 1u << handIndex; + } + + if (_temporaryIndices[2].TryGetValue(shape, out var legIndex)) + { + _temporaryValues[0] |= 1u << topIndex; + _temporaryValues[2] |= 1u << legIndex; + } + } + + foreach (var (shape, bottomIndex) in _temporaryIndices[2]) + { + if (_temporaryIndices[3].TryGetValue(shape, out var footIndex)) + { + _temporaryValues[2] |= 1u << bottomIndex; + _temporaryValues[3] |= 1u << footIndex; + } + } + } +} diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs new file mode 100644 index 00000000..987ed474 --- /dev/null +++ b/Penumbra/Meta/ShapeString.cs @@ -0,0 +1,89 @@ +using Lumina.Misc; +using Newtonsoft.Json; +using Penumbra.GameData.Files.PhybStructs; + +namespace Penumbra.Meta; + +[JsonConverter(typeof(Converter))] +public struct ShapeString : IEquatable +{ + public const int MaxLength = 30; + + public static readonly ShapeString Empty = new(); + + private FixedString32 _buffer; + + public int Count + => _buffer[31]; + + public int Length + => _buffer[31]; + + public override string ToString() + => Encoding.UTF8.GetString(_buffer[..Length]); + + public bool Equals(ShapeString other) + => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); + + public override bool Equals(object? obj) + => obj is ShapeString other && Equals(other); + + public override int GetHashCode() + => (int)Crc32.Get(_buffer[..Length]); + + public static bool operator ==(ShapeString left, ShapeString right) + => left.Equals(right); + + public static bool operator !=(ShapeString left, ShapeString right) + => !left.Equals(right); + + public static unsafe bool TryRead(byte* pointer, out ShapeString ret) + { + var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); + return TryRead(span, out ret); + } + + public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) + { + if (utf8.Length is 0 or > MaxLength) + { + ret = Empty; + return false; + } + + ret = Empty; + utf8.CopyTo(ret._buffer); + ret._buffer[utf8.Length] = 0; + ret._buffer[31] = (byte)utf8.Length; + return true; + } + + public static bool TryRead(ReadOnlySpan utf16, out ShapeString ret) + { + ret = Empty; + if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) + return false; + + ret._buffer[written] = 0; + ret._buffer[31] = (byte)written; + return true; + } + + private sealed class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + var value = serializer.Deserialize(reader); + if (!TryRead(value, out existingValue)) + throw new JsonReaderException($"Could not parse {value} into ShapeString."); + + return existingValue; + } + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 7f4c1b23..70636bbf 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -34,6 +34,7 @@ public class Penumbra : IDalamudPlugin public static readonly Logger Log = new(); public static MessageService Messager { get; private set; } = null!; + public static DynamisIpc Dynamis { get; private set; } = null!; private readonly ValidityChecker _validityChecker; private readonly ResidentResourceManager _residentResources; @@ -59,8 +60,9 @@ public class Penumbra : IDalamudPlugin _services = StaticServiceManager.CreateProvider(this, pluginInterface, Log); // Invoke the IPC Penumbra.Launching method before any hooks or other services are created. _services.GetService(); - Messager = _services.GetService(); - _validityChecker = _services.GetService(); + Messager = _services.GetService(); + Dynamis = _services.GetService(); + _validityChecker = _services.GetService(); _services.EnsureRequiredServices(); var startup = _services.GetService() @@ -228,6 +230,7 @@ public class Penumbra : IDalamudPlugin 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($"> **`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( diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 5d745419..e008752f 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,6 +81,9 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); + /// + public readonly ModelAttributeComputed ModelAttributeComputed = new(); + public void Dispose() { CollectionChange.Dispose(); @@ -105,5 +108,6 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); + ModelAttributeComputed.Dispose(); } } diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index eb9f05d9..3b25c1a9 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,11 +1,11 @@ using ImGuiNET; -using OtterGui.Extensions; -using OtterGui.Text; +using OtterGui.Extensions; +using OtterGui.Text; using Penumbra.GameData.Files; -using Penumbra.GameData.Files.AtchStructs; - -namespace Penumbra.UI.Tabs.Debug; - +using Penumbra.GameData.Files.AtchStructs; + +namespace Penumbra.UI.Tabs.Debug; + public static class AtchDrawer { public static void Draw(AtchFile file) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b7bc8edf..b4fa3b9f 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -108,6 +108,7 @@ public class DebugTab : Window, ITab, IUiService private readonly ObjectIdentification _objectIdentification; private readonly RenderTargetDrawer _renderTargetDrawer; private readonly ModMigratorDebug _modMigratorDebug; + private readonly ShapeInspector _shapeInspector; public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects, IClientState clientState, IDataManager dataManager, @@ -119,7 +120,7 @@ public class DebugTab : Window, ITab, IUiService Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, - ModMigratorDebug modMigratorDebug) + ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector) : base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse) { IsOpen = true; @@ -162,6 +163,7 @@ public class DebugTab : Window, ITab, IUiService _objectIdentification = objectIdentification; _renderTargetDrawer = renderTargetDrawer; _modMigratorDebug = modMigratorDebug; + _shapeInspector = shapeInspector; _objects = objects; _clientState = clientState; _dataManager = dataManager; @@ -508,37 +510,50 @@ public class DebugTab : Window, ITab, IUiService if (!ImGui.CollapsingHeader("Actors")) return; - _objects.DrawDebug(); - - using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX); - if (!table) - return; - - DrawSpecial("Current Player", _actors.GetCurrentPlayer()); - DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); - DrawSpecial("Current Card", _actors.GetCardPlayer()); - DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); - - foreach (var obj in _objects) + using (var objectTree = ImUtf8.TreeNode("Object Manager"u8)) { - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable($"0x{obj.Address:X}"); - ImGui.TableNextColumn(); - if (obj.Address != nint.Zero) - ImGuiUtil.CopyOnClickSelectable($"0x{(nint)((Character*)obj.Address)->GameObject.GetDrawObject():X}"); - var identifier = _actors.FromObject(obj, out _, false, true, false); - ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); - var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc - ? $"{identifier.DataId} | {obj.AsObject->BaseId}" - : identifier.DataId.ToString(); - ImGuiUtil.DrawTableColumn(id); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{*(nint*)obj.Address:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); - ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero - ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" - : "NULL"); + if (objectTree) + { + _objects.DrawDebug(); + + using var table = Table("##actors", 8, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + DrawSpecial("Current Player", _actors.GetCurrentPlayer()); + DrawSpecial("Current Inspect", _actors.GetInspectPlayer()); + DrawSpecial("Current Card", _actors.GetCardPlayer()); + DrawSpecial("Current Glamour", _actors.GetGlamourPlayer()); + + foreach (var obj in _objects) + { + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"{((GameObject*)obj.Address)->ObjectIndex}" : "NULL"); + ImGui.TableNextColumn(); + Penumbra.Dynamis.DrawPointer(obj.Address); + ImGui.TableNextColumn(); + if (obj.Address != nint.Zero) + Penumbra.Dynamis.DrawPointer((nint)((Character*)obj.Address)->GameObject.GetDrawObject()); + var identifier = _actors.FromObject(obj, out _, false, true, false); + ImGuiUtil.DrawTableColumn(_actors.ToString(identifier)); + var id = obj.AsObject->ObjectKind is ObjectKind.BattleNpc + ? $"{identifier.DataId} | {obj.AsObject->BaseId}" + : identifier.DataId.ToString(); + ImGuiUtil.DrawTableColumn(id); + ImGui.TableNextColumn(); + Penumbra.Dynamis.DrawPointer(obj.Address != nint.Zero ? *(nint*)obj.Address : nint.Zero); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero ? $"0x{obj.AsObject->EntityId:X}" : "NULL"); + ImGuiUtil.DrawTableColumn(obj.Address != nint.Zero + ? obj.AsObject->IsCharacter() ? $"Character: {obj.AsCharacter->ObjectKind}" : "No Character" + : "NULL"); + } + } + } + + using (var shapeTree = ImUtf8.TreeNode("Shape Inspector"u8)) + { + if (shapeTree) + _shapeInspector.Draw(); } return; @@ -1184,8 +1199,16 @@ public class DebugTab : Window, ITab, IUiService /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { - if (ImGui.CollapsingHeader("IPC")) - _ipcTester.Draw(); + if (!ImUtf8.CollapsingHeader("IPC"u8)) + return; + + using (var tree = ImUtf8.TreeNode("Dynamis"u8)) + { + if (tree) + Penumbra.Dynamis.DrawDebugInfo(); + } + + _ipcTester.Draw(); } /// Helper to print a property and its value in a 2-column table. diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs new file mode 100644 index 00000000..968bc484 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -0,0 +1,71 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; + +namespace Penumbra.UI.Tabs.Debug; + +public class ShapeInspector(ObjectManager objects) : IUiService +{ + private int _objectIndex = 0; + + public unsafe void Draw() + { + ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); + var actor = objects[0]; + if (!actor.IsCharacter) + { + ImUtf8.Text("No valid character."u8); + return; + } + + var human = actor.Model; + if (!human.IsHuman) + { + ImUtf8.Text("No valid character."u8); + return; + } + + using var table = ImUtf8.Table("##table"u8, 4, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + foreach (var slot in Enum.GetValues()) + { + ImUtf8.DrawTableColumn($"{(uint)slot:D2}"); + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[(int)slot]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if ((idx % 8) < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index b1f82a91..7b3a3c8b 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.Hooks.PostProcessing; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -50,6 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; + private readonly AttributeHooks _attributeHooks; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -61,7 +63,8 @@ public class SettingsTab : ITab, IUiService CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, - MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService) + MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, + AttributeHooks attributeHooks) { _pluginInterface = pluginInterface; _config = config; @@ -86,6 +89,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; + _attributeHooks = attributeHooks; } public void DrawHeader() @@ -807,6 +811,8 @@ 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 Advanced Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + _config.EnableAttributeHooks, _attributeHooks.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index dda6b305..4a162f8f 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -2,12 +2,6 @@ "version": 1, "dependencies": { "net9.0-windows7.0": { - "DalamudPackager": { - "type": "Direct", - "requested": "[12.0.0, )", - "resolved": "12.0.0", - "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" - }, "DotNet.ReproducibleBuilds": { "type": "Direct", "requested": "[1.2.25, )", From 6ad0b4299a29486af69a02d96bbf71cbbb77e939 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 17:46:53 +0200 Subject: [PATCH 686/865] Add shape meta manipulations and rework attribute hook. --- OtterGui | 2 +- Penumbra.GameData | 2 +- Penumbra.sln | 1 + Penumbra/Collections/Cache/CollectionCache.cs | 2 + Penumbra/Collections/Cache/MetaCache.cs | 9 +- Penumbra/Collections/Cache/ShpCache.cs | 109 ++++++++ .../Communication/ModelAttributeComputed.cs | 24 -- .../Hooks/PostProcessing/AttributeHook.cs | 85 ++++++ .../Hooks/PostProcessing/AttributeHooks.cs | 110 -------- .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 72 ++++- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 157 +++++++++++ Penumbra/Meta/ShapeManager.cs | 117 ++++----- Penumbra/Meta/ShapeString.cs | 33 ++- Penumbra/Services/CommunicatorService.cs | 4 - .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 5 +- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 247 ++++++++++++++++++ .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 114 +++++--- Penumbra/UI/Tabs/SettingsTab.cs | 64 +---- schemas/structs/manipulation.json | 12 +- schemas/structs/meta_enums.json | 4 + schemas/structs/meta_shp.json | 23 ++ 23 files changed, 900 insertions(+), 298 deletions(-) create mode 100644 Penumbra/Collections/Cache/ShpCache.cs delete mode 100644 Penumbra/Communication/ModelAttributeComputed.cs create mode 100644 Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs delete mode 100644 Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs create mode 100644 Penumbra/Meta/Manipulations/ShpIdentifier.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs create mode 100644 schemas/structs/meta_shp.json diff --git a/OtterGui b/OtterGui index 86b49242..f130c928 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 86b492422565abde2e8ad17c0295896a21c3439c +Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98 diff --git a/Penumbra.GameData b/Penumbra.GameData index 0ca50105..8e57c2e1 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0ca501050de72ee1cc7382dfae894f984ce241b6 +Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3 diff --git a/Penumbra.sln b/Penumbra.sln index e52045b0..642876ef 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -50,6 +50,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json schemas\structs\meta_imc.json = schemas\structs\meta_imc.json schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json + schemas\structs\meta_shp.json = schemas\structs\meta_shp.json schemas\structs\option.json = schemas\structs\option.json EndProjectSection EndProject diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index f6f038a1..c48a487c 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -245,6 +245,8 @@ public sealed class CollectionCache : IDisposable AddManipulation(mod, identifier, entry); foreach (var (identifier, entry) in files.Manipulations.Atch) AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Shp) + AddManipulation(mod, identifier, entry); foreach (var identifier in files.Manipulations.GlobalEqp) AddManipulation(mod, identifier, null!); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 7d8586c3..790dd3af 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -16,11 +16,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly RspCache Rsp = new(manager, collection); public readonly ImcCache Imc = new(manager, collection); public readonly AtchCache Atch = new(manager, collection); + public readonly ShpCache Shp = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -30,6 +31,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -41,6 +43,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Rsp.Reset(); Imc.Reset(); Atch.Reset(); + Shp.Reset(); GlobalEqp.Clear(); } @@ -57,6 +60,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Rsp.Dispose(); Imc.Dispose(); Atch.Dispose(); + Shp.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -71,6 +75,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod), RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), + ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -92,6 +97,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ImcIdentifier i => Imc.RevertMod(i, out mod), RspIdentifier i => Rsp.RevertMod(i, out mod), AtchIdentifier i => Atch.RevertMod(i, out mod), + ShpIdentifier i => Shp.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -108,6 +114,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e), RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), + ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs new file mode 100644 index 00000000..2e90052d --- /dev/null +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -0,0 +1,109 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) + => _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + + internal IReadOnlyDictionary State + => _shpData; + + internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> + { + private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); + + public bool All + { + get => _allIds[^1]; + set => _allIds[^1] = value; + } + + public bool this[HumanSlot slot] + { + get + { + if (slot is HumanSlot.Unknown) + return All; + + return _allIds[(int)slot]; + } + set + { + if (slot is HumanSlot.Unknown) + _allIds[^1] = value; + else + _allIds[(int)slot] = value; + } + } + + public bool Contains(HumanSlot slot, PrimaryId id) + => All || this[slot] || Contains((slot, id)); + + public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value) + { + if (slot is HumanSlot.Unknown) + { + var old = All; + All = value.Value; + return old != value.Value; + } + + if (!id.HasValue) + { + var old = this[slot]; + this[slot] = value.Value; + return old != value.Value; + } + + if (value.Value) + return Add((slot, id.Value)); + + return Remove((slot, id.Value)); + } + + public new void Clear() + { + base.Clear(); + _allIds.SetAll(false); + } + + public bool IsEmpty + => !_allIds.HasAnySet() && Count is 0; + } + + private readonly Dictionary _shpData = []; + + public void Reset() + { + Clear(); + _shpData.Clear(); + } + + protected override void Dispose(bool _) + => Clear(); + + protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + { + value = []; + _shpData.Add(identifier.Shape, value); + } + + value.TrySet(identifier.Slot, identifier.Id, entry); + } + + protected override void RevertModInternal(ShpIdentifier identifier) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + _shpData.Remove(identifier.Shape); + } +} diff --git a/Penumbra/Communication/ModelAttributeComputed.cs b/Penumbra/Communication/ModelAttributeComputed.cs deleted file mode 100644 index 389f56b6..00000000 --- a/Penumbra/Communication/ModelAttributeComputed.cs +++ /dev/null @@ -1,24 +0,0 @@ -using OtterGui.Classes; -using Penumbra.Collections; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; - -namespace Penumbra.Communication; - -/// -/// Triggered whenever a model recomputes its attribute masks. -/// -/// Parameter is the game object that recomputed its attributes. -/// Parameter is the draw object on which the recomputation was called. -/// Parameter is the collection associated with the game object. -/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. -/// -public sealed class ModelAttributeComputed() - : EventWrapper(nameof(ModelAttributeComputed)) -{ - public enum Priority - { - /// - ShapeManager = 0, - } -} diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs new file mode 100644 index 00000000..cad049ad --- /dev/null +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs @@ -0,0 +1,85 @@ +using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using OtterGui.Classes; +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; + +namespace Penumbra.Interop.Hooks.PostProcessing; + +/// +/// Triggered whenever a model recomputes its attribute masks. +/// +/// Parameter is the game object that recomputed its attributes. +/// Parameter is the draw object on which the recomputation was called. +/// Parameter is the collection associated with the game object. +/// Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. +/// +public sealed unsafe class AttributeHook : EventWrapper, IHookService +{ + public enum Priority + { + /// + ShapeManager = 0, + } + + private readonly CollectionResolver _resolver; + private readonly Configuration _config; + + public AttributeHook(HookManager hooks, Configuration config, CollectionResolver resolver) + : base("Update Model Attributes") + { + _config = config; + _resolver = resolver; + _task = hooks.CreateHook(Name, Sigs.UpdateAttributes, Detour, config.EnableCustomShapes); + } + + private readonly Task> _task; + + public nint Address + => _task.Result.Address; + + public void Enable() + => SetState(true); + + public void Disable() + => SetState(false); + + public void SetState(bool enabled) + { + if (_config.EnableCustomShapes == enabled) + return; + + _config.EnableCustomShapes = enabled; + _config.Save(); + if (enabled) + _task.Result.Enable(); + else + _task.Result.Disable(); + } + + + public Task Awaiter + => _task; + + public bool Finished + => _task.IsCompletedSuccessfully; + + private delegate void Delegate(Human* human); + + private void Detour(Human* human) + { + _task.Result.Original(human); + var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); + var identifiedActor = resolveData.AssociatedGameObject; + var identifiedCollection = resolveData.ModCollection; + Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{identifiedActor:X})."); + Invoke(identifiedActor, human, identifiedCollection); + } + + protected override void Dispose(bool disposing) + => _task.Result.Dispose(); +} diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs deleted file mode 100644 index 861962ee..00000000 --- a/Penumbra/Interop/Hooks/PostProcessing/AttributeHooks.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.Communication; -using Penumbra.GameData; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; -using Penumbra.Interop.PathResolving; -using Penumbra.Services; - -namespace Penumbra.Interop.Hooks.PostProcessing; - -public sealed unsafe class AttributeHooks : IRequiredService, IDisposable -{ - private delegate void SetupAttributes(Human* human, byte* data); - private delegate void AttributeUpdate(Human* human); - - private readonly Configuration _config; - private readonly ModelAttributeComputed _event; - private readonly CollectionResolver _resolver; - - private readonly AttributeHook[] _hooks; - private readonly Task> _updateHook; - private ModCollection _identifiedCollection = ModCollection.Empty; - private Actor _identifiedActor = Actor.Null; - private bool _inUpdate; - - public AttributeHooks(Configuration config, CommunicatorService communication, CollectionResolver resolver, HookManager hooks) - { - _config = config; - _event = communication.ModelAttributeComputed; - _resolver = resolver; - _hooks = - [ - new AttributeHook(this, hooks, Sigs.SetupTopModelAttributes, _config.EnableCustomShapes, HumanSlot.Body), - new AttributeHook(this, hooks, Sigs.SetupHandModelAttributes, _config.EnableCustomShapes, HumanSlot.Hands), - new AttributeHook(this, hooks, Sigs.SetupLegModelAttributes, _config.EnableCustomShapes, HumanSlot.Legs), - new AttributeHook(this, hooks, Sigs.SetupFootModelAttributes, _config.EnableCustomShapes, HumanSlot.Feet), - ]; - _updateHook = hooks.CreateHook("UpdateAttributes", Sigs.UpdateAttributes, UpdateAttributesDetour, - _config.EnableCustomShapes); - } - - private class AttributeHook - { - private readonly AttributeHooks _parent; - public readonly string Name; - public readonly Task> Hook; - public readonly uint ModelIndex; - public readonly HumanSlot Slot; - - public AttributeHook(AttributeHooks parent, HookManager hooks, string signature, bool enabled, HumanSlot slot) - { - _parent = parent; - Name = $"Setup{slot}Attributes"; - Slot = slot; - ModelIndex = slot.ToIndex(); - Hook = hooks.CreateHook(Name, signature, Detour, enabled); - } - - private void Detour(Human* human, byte* data) - { - Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{_parent._identifiedActor.Address:X})."); - Hook.Result.Original(human, data); - if (_parent is { _inUpdate: true, _identifiedActor.Valid: true }) - _parent._event.Invoke(_parent._identifiedActor, human, _parent._identifiedCollection, Slot); - } - } - - private void UpdateAttributesDetour(Human* human) - { - var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true); - _identifiedActor = resolveData.AssociatedGameObject; - _identifiedCollection = resolveData.ModCollection; - _inUpdate = true; - Penumbra.Log.Excessive($"[UpdateAttributes] Invoked on 0x{(ulong)human:X} (0x{_identifiedActor.Address:X})."); - _event.Invoke(_identifiedActor, human, _identifiedCollection, HumanSlot.Unknown); - _updateHook.Result.Original(human); - _inUpdate = false; - } - - public void SetState(bool enabled) - { - if (_config.EnableCustomShapes == enabled) - return; - - _config.EnableCustomShapes = enabled; - _config.Save(); - if (enabled) - { - foreach (var hook in _hooks) - hook.Hook.Result.Enable(); - _updateHook.Result.Enable(); - } - else - { - foreach (var hook in _hooks) - hook.Hook.Result.Disable(); - _updateHook.Result.Disable(); - } - } - - public void Dispose() - { - foreach (var hook in _hooks) - hook.Hook.Result.Dispose(); - _updateHook.Result.Dispose(); - } -} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index c897bb2a..13feba51 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -15,6 +15,7 @@ public enum MetaManipulationType : byte Rsp = 6, GlobalEqp = 7, Atch = 8, + Shp = 9, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index ca45c777..a7225067 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -18,6 +18,7 @@ public class MetaDictionary private readonly Dictionary _rsp = []; private readonly Dictionary _gmp = []; private readonly Dictionary _atch = []; + private readonly Dictionary _shp = []; private readonly HashSet _globalEqp = []; public IReadOnlyDictionary Imc @@ -41,6 +42,9 @@ public class MetaDictionary public IReadOnlyDictionary Atch => _atch; + public IReadOnlyDictionary Shp + => _shp; + public IReadOnlySet GlobalEqp => _globalEqp; @@ -56,6 +60,7 @@ public class MetaDictionary MetaManipulationType.Gmp => _gmp.Count, MetaManipulationType.Rsp => _rsp.Count, MetaManipulationType.Atch => _atch.Count, + MetaManipulationType.Shp => _shp.Count, MetaManipulationType.GlobalEqp => _globalEqp.Count, _ => 0, }; @@ -70,6 +75,7 @@ public class MetaDictionary GmpIdentifier i => _gmp.ContainsKey(i), ImcIdentifier i => _imc.ContainsKey(i), AtchIdentifier i => _atch.ContainsKey(i), + ShpIdentifier i => _shp.ContainsKey(i), RspIdentifier i => _rsp.ContainsKey(i), _ => false, }; @@ -84,6 +90,7 @@ public class MetaDictionary _rsp.Clear(); _gmp.Clear(); _atch.Clear(); + _shp.Clear(); _globalEqp.Clear(); } @@ -108,6 +115,7 @@ public class MetaDictionary && _rsp.SetEquals(other._rsp) && _gmp.SetEquals(other._gmp) && _atch.SetEquals(other._atch) + && _shp.SetEquals(other._shp) && _globalEqp.SetEquals(other._globalEqp); public IEnumerable Identifiers @@ -118,6 +126,7 @@ public class MetaDictionary .Concat(_gmp.Keys.Cast()) .Concat(_rsp.Keys.Cast()) .Concat(_atch.Keys.Cast()) + .Concat(_shp.Keys.Cast()) .Concat(_globalEqp.Cast()); #region TryAdd @@ -191,6 +200,15 @@ public class MetaDictionary return true; } + public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry) + { + if (!_shp.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { if (!_globalEqp.Add(identifier)) @@ -273,6 +291,15 @@ public class MetaDictionary return true; } + public bool Update(ShpIdentifier identifier, in ShpEntry entry) + { + if (!_shp.ContainsKey(identifier)) + return false; + + _shp[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -298,6 +325,9 @@ public class MetaDictionary public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) => _atch.TryGetValue(identifier, out value); + public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) + => _shp.TryGetValue(identifier, out value); + #endregion public bool Remove(IMetaIdentifier identifier) @@ -312,6 +342,7 @@ public class MetaDictionary ImcIdentifier i => _imc.Remove(i), RspIdentifier i => _rsp.Remove(i), AtchIdentifier i => _atch.Remove(i), + ShpIdentifier i => _shp.Remove(i), _ => false, }; if (ret) @@ -344,6 +375,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._atch) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._shp) + TryAdd(identifier, entry); + foreach (var identifier in manips._globalEqp) TryAdd(identifier); } @@ -393,13 +427,19 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; return false; } - failedIdentifier = default; + failedIdentifier = null; return true; } @@ -412,8 +452,9 @@ public class MetaDictionary _rsp.SetTo(other._rsp); _gmp.SetTo(other._gmp); _atch.SetTo(other._atch); + _shp.SetTo(other._shp); _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; } public void UpdateTo(MetaDictionary other) @@ -425,8 +466,9 @@ public class MetaDictionary _rsp.UpdateTo(other._rsp); _gmp.UpdateTo(other._gmp); _atch.UpdateTo(other._atch); + _shp.UpdateTo(other._shp); _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _globalEqp.Count; + Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; } #endregion @@ -514,6 +556,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(ShpIdentifier identifier, ShpEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Shp.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -543,6 +595,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(AtchIdentifier) && typeof(TEntry) == typeof(AtchEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -588,6 +642,7 @@ public class MetaDictionary SerializeTo(array, value._rsp); SerializeTo(array, value._gmp); SerializeTo(array, value._atch); + SerializeTo(array, value._shp); SerializeTo(array, value._globalEqp); array.WriteTo(writer); } @@ -685,6 +740,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid ATCH Manipulation encountered."); break; } + case MetaManipulationType.Shp: + { + var identifier = ShpIdentifier.FromJson(manip); + var entry = new ShpEntry(manip["Entry"]?.Value() ?? true); + if (identifier.HasValue) + dict.TryAdd(identifier.Value, entry); + else + Penumbra.Log.Warning("Invalid SHP Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); @@ -716,6 +781,7 @@ public class MetaDictionary _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + _shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); Count = cache.Count; } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs new file mode 100644 index 00000000..fffa51ba --- /dev/null +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -0,0 +1,157 @@ +using Lumina.Models.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape) + : IComparable, IMetaIdentifier +{ + public int CompareTo(ShpIdentifier other) + { + var slotComparison = Slot.CompareTo(other.Slot); + if (slotComparison is not 0) + return slotComparison; + + if (Id.HasValue) + { + if (other.Id.HasValue) + { + var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id); + if (idComparison is not 0) + return idComparison; + } + else + { + return -1; + } + } + else if (other.Id.HasValue) + { + return 1; + } + + + return Shape.CompareTo(other.Shape); + } + + public override string ToString() + => $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}"; + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing for now since it depends entirely on the shape key. + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) + return false; + + if (Slot is HumanSlot.Unknown && Id is not null) + return false; + + return ValidateCustomShapeString(Shape); + } + + public static bool ValidateCustomShapeString(ReadOnlySpan shape) + { + // "shp_xx_y" + if (shape.Length < 8) + return false; + + if (shape[0] is not (byte)'s' + || shape[1] is not (byte)'h' + || shape[2] is not (byte)'p' + || shape[3] is not (byte)'_' + || shape[6] is not (byte)'_') + return false; + + return true; + } + + public static unsafe bool ValidateCustomShapeString(byte* shape) + { + // "shp_xx_y" + if (shape is null) + return false; + + if (*shape++ is not (byte)'s' + || *shape++ is not (byte)'h' + || *shape++ is not (byte)'p' + || *shape++ is not (byte)'_' + || *shape++ is 0 + || *shape++ is 0 + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public static bool ValidateCustomShapeString(in ShapeString shape) + { + // "shp_xx_y" + if (shape.Length < 8) + return false; + + var span = shape.AsSpan; + if (span[0] is not (byte)'s' + || span[1] is not (byte)'h' + || span[2] is not (byte)'p' + || span[3] is not (byte)'_' + || span[6] is not (byte)'_') + return false; + + return true; + } + + public JObject AddToJson(JObject jObj) + { + if (Slot is not HumanSlot.Unknown) + jObj["Slot"] = Slot.ToString(); + if (Id.HasValue) + jObj["Id"] = Id.Value.Id.ToString(); + jObj["Shape"] = Shape.ToString(); + return jObj; + } + + public static ShpIdentifier? FromJson(JObject jObj) + { + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var shape = jObj["Shape"]?.ToObject(); + if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) + return null; + + var identifier = new ShpIdentifier(slot, id, shapeString); + return identifier.Validate() ? identifier : null; + } + + public MetaManipulationType Type + => MetaManipulationType.Shp; +} + +[JsonConverter(typeof(Converter))] +public readonly record struct ShpEntry(bool Value) +{ + public static readonly ShpEntry True = new(true); + public static readonly ShpEntry False = new(false); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ShpEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override ShpEntry ReadJson(JsonReader reader, Type objectType, ShpEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index 4356086a..ec8ddb50 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,24 +1,30 @@ +using System.Reflection.Metadata.Ecma335; using OtterGui.Services; using Penumbra.Collections; -using Penumbra.Communication; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; -using Penumbra.Services; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Meta.Manipulations; namespace Penumbra.Meta; public class ShapeManager : IRequiredService, IDisposable { - public const int NumSlots = 4; - private readonly CommunicatorService _communicator; + public const int NumSlots = 14; + public const int ModelSlotSize = 18; + private readonly AttributeHook _attributeHook; - private static ReadOnlySpan UsedModels - => [1, 2, 3, 4]; + public static ReadOnlySpan UsedModels + => + [ + HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, + HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, + ]; - public ShapeManager(CommunicatorService communicator) + public ShapeManager(AttributeHook attributeHook) { - _communicator = communicator; - _communicator.ModelAttributeComputed.Subscribe(OnAttributeComputed, ModelAttributeComputed.Priority.ShapeManager); + _attributeHook = attributeHook; + _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager); } private readonly Dictionary[] _temporaryIndices = @@ -27,38 +33,30 @@ public class ShapeManager : IRequiredService, IDisposable private readonly uint[] _temporaryMasks = new uint[NumSlots]; private readonly uint[] _temporaryValues = new uint[NumSlots]; - private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection, HumanSlot slot) - { - int index; - switch (slot) - { - case HumanSlot.Unknown: - ResetCache(model); - return; - case HumanSlot.Body: index = 0; break; - case HumanSlot.Hands: index = 1; break; - case HumanSlot.Legs: index = 2; break; - case HumanSlot.Feet: index = 3; break; - default: return; - } + public void Dispose() + => _attributeHook.Unsubscribe(OnAttributeComputed); - if (_temporaryMasks[index] is 0) + private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) + { + ComputeCache(model, collection); + for (var i = 0; i < NumSlots; ++i) + { + if (_temporaryMasks[i] is 0) + continue; + + var modelIndex = UsedModels[i]; + var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask; + var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i]; + Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); + model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask; + } + } + + private unsafe void ComputeCache(Model human, ModCollection collection) + { + if (!collection.HasCache) return; - var modelIndex = UsedModels[index]; - var currentMask = model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask; - var newMask = (currentMask & ~_temporaryMasks[index]) | _temporaryValues[index]; - Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); - model.AsHuman->Models[modelIndex]->EnabledShapeKeyIndexMask = newMask; - } - - public void Dispose() - { - _communicator.ModelAttributeComputed.Unsubscribe(OnAttributeComputed); - } - - private unsafe void ResetCache(Model human) - { for (var i = 0; i < NumSlots; ++i) { _temporaryMasks[i] = 0; @@ -66,17 +64,20 @@ public class ShapeManager : IRequiredService, IDisposable _temporaryIndices[i].Clear(); var modelIndex = UsedModels[i]; - var model = human.AsHuman->Models[modelIndex]; + var model = human.AsHuman->Models[modelIndex.ToIndex()]; if (model is null || model->ModelResourceHandle is null) continue; ref var shapes = ref model->ModelResourceHandle->Shapes; - foreach (var (shape, index) in shapes.Where(kvp => CheckShapes(kvp.Key.AsSpan(), modelIndex))) + foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) { if (ShapeString.TryRead(shape.Value, out var shapeString)) { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); + if (collection.MetaCache!.Shp.State.Count > 0 + && collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set)) + _temporaryValues[i] |= (ushort)(1 << index); } else { @@ -85,42 +86,32 @@ public class ShapeManager : IRequiredService, IDisposable } } - UpdateMasks(); + UpdateDefaultMasks(); } - private static bool CheckShapes(ReadOnlySpan shape, byte index) - => index switch - { - 1 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_wr_"u8), - 2 => shape.StartsWith("shp_wr_"u8), - 3 => shape.StartsWith("shp_wa_"u8) || shape.StartsWith("shp_an"u8), - 4 => shape.StartsWith("shp_an"u8), - _ => false, - }; - - private void UpdateMasks() + private void UpdateDefaultMasks() { - foreach (var (shape, topIndex) in _temporaryIndices[0]) + foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (_temporaryIndices[1].TryGetValue(shape, out var handIndex)) + if (shape[4] is (byte)'w' && shape[5] is (byte)'r' && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { - _temporaryValues[0] |= 1u << topIndex; - _temporaryValues[1] |= 1u << handIndex; + _temporaryValues[1] |= 1u << topIndex; + _temporaryValues[2] |= 1u << handIndex; } - if (_temporaryIndices[2].TryGetValue(shape, out var legIndex)) + if (shape[4] is (byte)'w' && shape[5] is (byte)'a' && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { - _temporaryValues[0] |= 1u << topIndex; - _temporaryValues[2] |= 1u << legIndex; + _temporaryValues[1] |= 1u << topIndex; + _temporaryValues[3] |= 1u << legIndex; } } - foreach (var (shape, bottomIndex) in _temporaryIndices[2]) + foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (_temporaryIndices[3].TryGetValue(shape, out var footIndex)) + if (shape[4] is (byte)'a' && shape[5] is (byte)'n' && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { - _temporaryValues[2] |= 1u << bottomIndex; - _temporaryValues[3] |= 1u << footIndex; + _temporaryValues[3] |= 1u << bottomIndex; + _temporaryValues[4] |= 1u << footIndex; } } } diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs index 987ed474..5b6f9c52 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeString.cs @@ -1,11 +1,12 @@ using Lumina.Misc; using Newtonsoft.Json; using Penumbra.GameData.Files.PhybStructs; +using Penumbra.String.Functions; namespace Penumbra.Meta; [JsonConverter(typeof(Converter))] -public struct ShapeString : IEquatable +public struct ShapeString : IEquatable, IComparable { public const int MaxLength = 30; @@ -22,6 +23,20 @@ public struct ShapeString : IEquatable public override string ToString() => Encoding.UTF8.GetString(_buffer[..Length]); + public byte this[int index] + => _buffer[index]; + + public unsafe ReadOnlySpan AsSpan + { + get + { + fixed (void* ptr = &this) + { + return new ReadOnlySpan(ptr, Length); + } + } + } + public bool Equals(ShapeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); @@ -43,6 +58,14 @@ public struct ShapeString : IEquatable return TryRead(span, out ret); } + public unsafe int CompareTo(ShapeString other) + { + fixed (void* lhs = &this) + { + return ByteStringFunctions.Compare((byte*)lhs, Length, (byte*)&other, other.Length); + } + } + public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) { if (utf8.Length is 0 or > MaxLength) @@ -69,6 +92,14 @@ public struct ShapeString : IEquatable return true; } + public void ForceLength(byte length) + { + if (length > MaxLength) + length = MaxLength; + _buffer[length] = 0; + _buffer[31] = length; + } + private sealed class Converter : JsonConverter { public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index e008752f..5d745419 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -81,9 +81,6 @@ public class CommunicatorService : IDisposable, IService /// public readonly ResolvedFileChanged ResolvedFileChanged = new(); - /// - public readonly ModelAttributeComputed ModelAttributeComputed = new(); - public void Dispose() { CollectionChange.Dispose(); @@ -108,6 +105,5 @@ public class CommunicatorService : IDisposable, IService ChangedItemClick.Dispose(); SelectTab.Dispose(); ResolvedFileChanged.Dispose(); - ModelAttributeComputed.Dispose(); } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index d1c7cd52..70b5f83b 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -11,7 +11,8 @@ public class MetaDrawers( GmpMetaDrawer gmp, ImcMetaDrawer imc, RspMetaDrawer rsp, - AtchMetaDrawer atch) : IService + AtchMetaDrawer atch, + ShpMetaDrawer shp) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -21,6 +22,7 @@ public class MetaDrawers( public readonly ImcMetaDrawer Imc = imc; public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; public readonly AtchMetaDrawer Atch = atch; + public readonly ShpMetaDrawer Shp = shp; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -32,6 +34,7 @@ public class MetaDrawers( MetaManipulationType.Gmp => Gmp, MetaManipulationType.Rsp => Rsp, MetaManipulationType.Atch => Atch, + MetaManipulationType.Shp => Shp, MetaManipulationType.GlobalEqp => GlobalEqp, _ => null, }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs new file mode 100644 index 00000000..4be6e6aa --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -0,0 +1,247 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.GameData.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Shape Keys (SHP)###SHP"u8; + + private ShapeString _buffer = ShapeString.TryRead("shp_"u8, out var s) ? s : ShapeString.Empty; + private bool _identifierValid; + + public override int NumColumns + => 6; + + public override float ColumnHeight + => ImUtf8.FrameHeightSpacing; + + protected override void Initialize() + { + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty); + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current SHP manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var tt = canAdd + ? "Stage this edit."u8 + : _identifierValid + ? "This entry does not contain a valid shape key."u8 + : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, ShpEntry.True); + + DrawIdentifierInput(ref Identifier); + DrawEntry(ref Entry, true); + } + + protected override void DrawEntry(ShpIdentifier identifier, ShpEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + if (DrawEntry(ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(ShpIdentifier, ShpEntry)> Enumerate() + => Editor.Shp + .OrderBy(kvp => kvp.Key.Shape) + .ThenBy(kvp => kvp.Key.Slot) + .ThenBy(kvp => kvp.Key.Id) + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Shp.Count; + + private bool DrawIdentifierInput(ref ShpIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawHumanSlot(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + return changes; + } + + private static void DrawIdentifier(ShpIdentifier identifier) + { + ImGui.TableNextColumn(); + + ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor); + ImUtf8.HoverTooltip("Model Slot"u8); + + ImGui.TableNextColumn(); + if (identifier.Id.HasValue) + ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); + else + ImUtf8.TextFramed("All IDs"u8, FrameColor); + ImUtf8.HoverTooltip("Primary ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); + } + + private static bool DrawEntry(ref ShpEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var value = entry.Value; + var changes = ImUtf8.Checkbox("##shpEntry"u8, ref value); + if (changes) + entry = new ShpEntry(value); + ImUtf8.HoverTooltip("Whether to enable or disable this shape key for the selected items."); + return changes; + } + + public static bool DrawPrimaryId(ref ShpIdentifier identifier, float unscaledWidth = 100) + { + var allSlots = identifier.Slot is HumanSlot.Unknown; + var all = !identifier.Id.HasValue; + var ret = false; + using (ImRaii.Disabled(allSlots)) + { + if (ImUtf8.Checkbox("##shpAll"u8, ref all)) + { + identifier = identifier with { Id = all ? null : 0 }; + ret = true; + } + } + + ImUtf8.HoverTooltip(allSlots ? "When using all slots, you also need to use all IDs."u8 : "Enable this shape key for all model IDs."u8); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (all) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f), + new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled)); + } + else + { + if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, + ExpandedEqpGmpBase.Count - 1, + false)) + { + identifier = identifier with { Id = setId }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8); + + return ret; + } + + public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##shpSlot"u8, SlotName(identifier.Slot))) + { + if (combo) + foreach (var slot in AvailableSlots) + { + if (!ImUtf8.Selectable(SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot) + continue; + + ret = true; + if (slot is HumanSlot.Unknown) + identifier = identifier with + { + Id = null, + Slot = slot, + }; + else + identifier = identifier with { Slot = slot }; + } + } + + ImUtf8.HoverTooltip("Model Slot"u8); + return ret; + } + + public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##shpShape"u8, span, out int newLength, "Shape Key..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = ShpIdentifier.ValidateCustomShapeString(buffer); + if (valid) + identifier = identifier with { Shape = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Supported shape keys need to have the format `shp_xx_*` and a maximum length of 30 characters."u8); + return ret; + } + + private static ReadOnlySpan AvailableSlots + => + [ + HumanSlot.Unknown, + HumanSlot.Head, + HumanSlot.Body, + HumanSlot.Hands, + HumanSlot.Legs, + HumanSlot.Feet, + HumanSlot.Ears, + HumanSlot.Neck, + HumanSlot.Wrists, + HumanSlot.RFinger, + HumanSlot.LFinger, + HumanSlot.Glasses, + HumanSlot.Hair, + HumanSlot.Face, + HumanSlot.Ear, + ]; + + private static ReadOnlySpan SlotName(HumanSlot slot) + => slot switch + { + HumanSlot.Unknown => "All Slots"u8, + HumanSlot.Head => "Equipment: Head"u8, + HumanSlot.Body => "Equipment: Body"u8, + HumanSlot.Hands => "Equipment: Hands"u8, + HumanSlot.Legs => "Equipment: Legs"u8, + HumanSlot.Feet => "Equipment: Feet"u8, + HumanSlot.Ears => "Equipment: Ears"u8, + HumanSlot.Neck => "Equipment: Neck"u8, + HumanSlot.Wrists => "Equipment: Wrists"u8, + HumanSlot.RFinger => "Equipment: Right Finger"u8, + HumanSlot.LFinger => "Equipment: Left Finger"u8, + HumanSlot.Glasses => "Equipment: Glasses"u8, + HumanSlot.Hair => "Customization: Hair"u8, + HumanSlot.Face => "Customization: Face"u8, + HumanSlot.Ear => "Customization: Ears"u8, + _ => "Unknown"u8, + }; +} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 68424ae9..70a15373 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -61,6 +61,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Gmp); DrawEditHeader(MetaManipulationType.Rsp); DrawEditHeader(MetaManipulationType.Atch); + DrawEditHeader(MetaManipulationType.Shp); DrawEditHeader(MetaManipulationType.GlobalEqp); } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 968bc484..5292bd17 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -5,10 +5,12 @@ using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Meta; namespace Penumbra.UI.Tabs.Debug; -public class ShapeInspector(ObjectManager objects) : IUiService +public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService { private int _objectIndex = 0; @@ -29,42 +31,92 @@ public class ShapeInspector(ObjectManager objects) : IUiService return; } - using var table = ImUtf8.Table("##table"u8, 4, ImGuiTableFlags.RowBg); - if (!table) - return; - - ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); - ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); - - var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); - foreach (var slot in Enum.GetValues()) + var data = resolver.IdentifyCollection(actor.AsObject, true); + using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})")) { - ImUtf8.DrawTableColumn($"{(uint)slot:D2}"); - ImGui.TableNextColumn(); - var model = human.AsHuman->Models[(int)slot]; - Penumbra.Dynamis.DrawPointer((nint)model); - if (model is not null) + if (treeNode1.Success && data.ModCollection.HasCache) { - var mask = model->EnabledShapeKeyIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch); + + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) { - var disabled = (mask & (1u << idx)) is 0; - using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); - ImUtf8.Text(shape.AsSpan()); - ImGui.SameLine(0, 0); - ImUtf8.Text(", "u8); - if ((idx % 8) < 7) - ImGui.SameLine(0, 0); + ImUtf8.DrawTableColumn(shape.AsSpan); + if (set.All) + { + ImUtf8.DrawTableColumn("All"u8); + } + else + { + ImGui.TableNextColumn(); + foreach (var slot in ShapeManager.UsedModels) + { + if (!set[slot]) + continue; + + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var item in set.Where(i => !set[i.Slot])) + { + ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + } } } - else + } + + using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8)) + { + if (treeNode2) { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } } } } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 7b3a3c8b..cb22b54a 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -51,7 +51,7 @@ public class SettingsTab : ITab, IUiService private readonly MigrationSectionDrawer _migrationDrawer; private readonly CollectionAutoSelector _autoSelector; private readonly CleanupService _cleanupService; - private readonly AttributeHooks _attributeHooks; + private readonly AttributeHook _attributeHook; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -64,7 +64,7 @@ public class SettingsTab : ITab, IUiService DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, - AttributeHooks attributeHooks) + AttributeHook attributeHook) { _pluginInterface = pluginInterface; _config = config; @@ -89,7 +89,7 @@ public class SettingsTab : ITab, IUiService _migrationDrawer = migrationDrawer; _autoSelector = autoSelector; _cleanupService = cleanupService; - _attributeHooks = attributeHooks; + _attributeHook = attributeHook; } public void DrawHeader() @@ -525,55 +525,6 @@ public class SettingsTab : ITab, IUiService ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab."); } - private float _absoluteSelectorSize = float.NaN; - - /// Draw a selector for the absolute size of the mod selector in pixels. - private void DrawAbsoluteSizeSelector() - { - if (float.IsNaN(_absoluteSelectorSize)) - _absoluteSelectorSize = _config.ModSelectorAbsoluteSize; - - if (ImGuiUtil.DragFloat("##absoluteSize", ref _absoluteSelectorSize, UiHelpers.InputTextWidth.X, 1, - Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f") - && _absoluteSelectorSize != _config.ModSelectorAbsoluteSize) - { - _config.ModSelectorAbsoluteSize = _absoluteSelectorSize; - _config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Mod Selector Absolute Size", - "The minimal absolute size of the mod selector in the mod tab in pixels."); - } - - private int _relativeSelectorSize = int.MaxValue; - - /// Draw a selector for the relative size of the mod selector as a percentage and a toggle to enable relative sizing. - private void DrawRelativeSizeSelector() - { - var scaleModSelector = _config.ScaleModSelector; - if (ImGui.Checkbox("Scale Mod Selector With Window Size", ref scaleModSelector)) - { - _config.ScaleModSelector = scaleModSelector; - _config.Save(); - } - - ImGui.SameLine(); - if (_relativeSelectorSize == int.MaxValue) - _relativeSelectorSize = _config.ModSelectorScaledSize; - if (ImGuiUtil.DragInt("##relativeSize", ref _relativeSelectorSize, UiHelpers.InputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, - Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%") - && _relativeSelectorSize != _config.ModSelectorScaledSize) - { - _config.ModSelectorScaledSize = _relativeSelectorSize; - _config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker("Mod Selector Relative Size", - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window."); - } - private void DrawRenameSettings() { ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); @@ -607,8 +558,6 @@ public class SettingsTab : ITab, IUiService private void DrawModSelectorSettings() { DrawFolderSortType(); - DrawAbsoluteSizeSelector(); - DrawRelativeSizeSelector(); DrawRenameSettings(); Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", _config.OpenFoldersByDefault, v => @@ -626,7 +575,8 @@ public class SettingsTab : ITab, IUiService _config.Save(); }); Widget.DoubleModifierSelector("Incognito Modifier", - "A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", UiHelpers.InputTextWidth.X, + "A modifier you need to hold while clicking the Incognito or Temporary Settings Mode button for it to take effect.", + UiHelpers.InputTextWidth.X, _config.IncognitoModifier, v => { @@ -811,8 +761,8 @@ 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 Advanced Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", - _config.EnableAttributeHooks, _attributeHooks.SetState); + Checkbox("Enable Custom Shape Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + _config.EnableCustomShapes, _attributeHook.SetState); DrawWaitForPluginsReflection(); DrawEnableHttpApiBox(); DrawEnableDebugModeBox(); diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json index 4a41dbe2..55fc5cad 100644 --- a/schemas/structs/manipulation.json +++ b/schemas/structs/manipulation.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "Type": { - "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch" ] + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ] }, "Manipulation": { "type": "object" @@ -90,6 +90,16 @@ "$ref": "meta_atch.json" } } + }, + { + "properties": { + "Type": { + "const": "Shp" + }, + "Manipulation": { + "$ref": "meta_shp.json" + } + } } ] } diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json index 747da849..2fc65a0d 100644 --- a/schemas/structs/meta_enums.json +++ b/schemas/structs/meta_enums.json @@ -5,6 +5,10 @@ "$anchor": "EquipSlot", "enum": [ "Unknown", "MainHand", "OffHand", "Head", "Body", "Hands", "Belt", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "BothHand", "LFinger", "HeadBody", "BodyHandsLegsFeet", "SoulCrystal", "LegsFeet", "FullBody", "BodyHands", "BodyLegsFeet", "ChestHands", "Nothing", "All" ] }, + "HumanSlot": { + "$anchor": "HumanSlot", + "enum": [ "Head", "Body", "Hands", "Legs", "Feet", "Ears", "Neck", "Wrists", "RFinger", "LFinger", "Hair", "Face", "Ear", "Glasses", "Unknown" ] + }, "Gender": { "$anchor": "Gender", "enum": [ "Unknown", "Male", "Female", "MaleNpc", "FemaleNpc" ] diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json new file mode 100644 index 00000000..e6b66420 --- /dev/null +++ b/schemas/structs/meta_shp.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "boolean" + }, + "Slot": { + "$ref": "meta_enums.json#HumanSlot" + }, + "Id": { + "$ref": "meta_enums.json#U16" + }, + "Shape": { + "type": "string", + "minLength": 8, + "maxLength": 30 + } + }, + "required": [ + "Shape" + ] +} From 480942339f4bff0d30e7afcb84852d3f318eb47a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 17:47:32 +0200 Subject: [PATCH 687/865] Add draggable mod selector width. --- Penumbra/EphemeralConfig.cs | 5 +++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 33 +++++++++++++++----- Penumbra/UI/Tabs/ModsTab.cs | 15 +-------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 678e53ad..ecb0218f 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,6 +1,7 @@ using Dalamud.Interface.ImGuiNotification; using Newtonsoft.Json; using OtterGui.Classes; +using OtterGui.FileSystem.Selector; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; @@ -23,6 +24,10 @@ public class EphemeralConfig : ISavable, IDisposable, IService [JsonIgnore] private readonly ModPathChanged _modPathChanged; + public float CurrentModSelectorWidth { get; set; } = 200f; + public float ModSelectorMinimumScale { get; set; } = 0.1f; + public float ModSelectorMaximumScale { get; set; } = 0.5f; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public bool DebugSeparateWindow { get; set; } = false; diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 6586747c..2dff19ab 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -131,18 +131,41 @@ public sealed class ModFileSystemSelector : FileSystemSelector m.Extensions.Any(e => ValidModExtensions.Contains(e.ToLowerInvariant())), m => { ImUtf8.Text($"Dragging mods for import:\n\t{string.Join("\n\t", m.Files.Select(Path.GetFileName))}"); return true; }); - base.Draw(width); + base.Draw(); if (_dragDrop.CreateImGuiTarget("ModDragDrop", out var files, out _)) _modImportManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f.ToLowerInvariant())))); } + protected override float CurrentWidth + => _config.Ephemeral.CurrentModSelectorWidth * ImUtf8.GlobalScale; + + protected override float MinimumAbsoluteRemainder + => 550 * ImUtf8.GlobalScale; + + protected override float MinimumScaling + => _config.Ephemeral.ModSelectorMinimumScale; + + protected override float MaximumScaling + => _config.Ephemeral.ModSelectorMaximumScale; + + protected override void SetSize(Vector2 size) + { + base.SetSize(size); + var adaptedSize = MathF.Round(size.X / ImUtf8.GlobalScale); + if (adaptedSize == _config.Ephemeral.CurrentModSelectorWidth) + return; + + _config.Ephemeral.CurrentModSelectorWidth = adaptedSize; + _config.Ephemeral.Save(); + } + public override void Dispose() { base.Dispose(); @@ -651,14 +674,10 @@ public sealed class ModFileSystemSelector : FileSystemSelector Get the correct size for the mod selector based on current config. - public static float GetModSelectorSize(Configuration config) - { - var absoluteSize = Math.Clamp(config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, - Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100)); - var relativeSize = config.ScaleModSelector - ? Math.Clamp(config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) - : 0; - return MathF.Round(config.ScaleModSelector - ? Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100) - : absoluteSize); - } - private void DrawRedrawLine() { if (config.HideRedrawBar) From 70295b7a6bca0c5fb1358cf5f396dee4173a5e23 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 15 May 2025 15:50:16 +0000 Subject: [PATCH 688/865] [CI] Updating repo.json for testing_1.3.6.9 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 8e88ad52..13d91e5c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.8", + "TestingAssemblyVersion": "1.3.6.9", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.9/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From c0dcfdd83587a5c27bcb707f391ad9e53ed752fb Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 15 May 2025 22:23:33 +0200 Subject: [PATCH 689/865] Update shape string format. --- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 31 ++++--------------- Penumbra/Meta/ShapeManager.cs | 10 ++++-- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 4 +-- schemas/structs/meta_shp.json | 5 +-- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index fffa51ba..c642167f 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,4 +1,3 @@ -using Lumina.Models.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; @@ -61,34 +60,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return ValidateCustomShapeString(Shape); } - public static bool ValidateCustomShapeString(ReadOnlySpan shape) - { - // "shp_xx_y" - if (shape.Length < 8) - return false; - - if (shape[0] is not (byte)'s' - || shape[1] is not (byte)'h' - || shape[2] is not (byte)'p' - || shape[3] is not (byte)'_' - || shape[6] is not (byte)'_') - return false; - - return true; - } - public static unsafe bool ValidateCustomShapeString(byte* shape) { - // "shp_xx_y" + // "shpx_*" if (shape is null) return false; if (*shape++ is not (byte)'s' || *shape++ is not (byte)'h' || *shape++ is not (byte)'p' - || *shape++ is not (byte)'_' - || *shape++ is 0 - || *shape++ is 0 + || *shape++ is not (byte)'x' || *shape++ is not (byte)'_' || *shape is 0) return false; @@ -98,16 +79,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape public static bool ValidateCustomShapeString(in ShapeString shape) { - // "shp_xx_y" - if (shape.Length < 8) + // "shpx_*" + if (shape.Length < 6) return false; var span = shape.AsSpan; if (span[0] is not (byte)'s' || span[1] is not (byte)'h' || span[2] is not (byte)'p' - || span[3] is not (byte)'_' - || span[6] is not (byte)'_') + || span[3] is not (byte)'x' + || span[4] is not (byte)'_') return false; return true; diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index ec8ddb50..dc3e1a1c 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -93,13 +93,13 @@ public class ShapeManager : IRequiredService, IDisposable { foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (shape[4] is (byte)'w' && shape[5] is (byte)'r' && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) + if (CheckCenter(shape, 'w', 'r') && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; } - if (shape[4] is (byte)'w' && shape[5] is (byte)'a' && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) + if (CheckCenter(shape, 'w', 'a') && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; @@ -108,11 +108,15 @@ public class ShapeManager : IRequiredService, IDisposable foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (shape[4] is (byte)'a' && shape[5] is (byte)'n' && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) + if (CheckCenter(shape, 'a', 'n') && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; } } } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool CheckCenter(in ShapeString shape, char first, char second) + => shape.Length > 8 && shape[4] == first && shape[5] == second && shape[6] is (byte)'_'; } diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 4be6e6aa..2c99af02 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,7 +19,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shp_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; private bool _identifierValid; public override int NumColumns @@ -200,7 +200,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } } - ImUtf8.HoverTooltip("Supported shape keys need to have the format `shp_xx_*` and a maximum length of 30 characters."u8); + ImUtf8.HoverTooltip("Supported shape keys need to have the format `shpx_*` and a maximum length of 30 characters."u8); return ret; } diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index e6b66420..197f3104 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -13,8 +13,9 @@ }, "Shape": { "type": "string", - "minLength": 8, - "maxLength": 30 + "minLength": 5, + "maxLength": 30, + "pattern": "^shpx_" } }, "required": [ From f1448ed947039cc9a9e235e6da4745cdcde483cd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 16 May 2025 00:25:13 +0200 Subject: [PATCH 690/865] Add conditional connector shapes. --- Penumbra/Collections/Cache/ShpCache.cs | 79 +++++++- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 62 +++++- Penumbra/Meta/ShapeManager.cs | 58 ++++-- Penumbra/Meta/ShapeString.cs | 16 ++ .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 76 +++++++- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 183 ++++++++++-------- schemas/structs/meta_shp.json | 6 + 7 files changed, 360 insertions(+), 120 deletions(-) diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 2e90052d..eaf949d9 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -13,7 +13,23 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) internal IReadOnlyDictionary State => _shpData; - internal sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> + internal IEnumerable<(ShapeString, IReadOnlyDictionary)> ConditionState + => _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary)kvp.Value)); + + public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary? dict) + { + if (_conditionalSet.TryGetValue(condition, out var d)) + { + dict = d; + return true; + } + + dict = null; + return false; + } + + + public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -76,12 +92,14 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) => !_allIds.HasAnySet() && Count is 0; } - private readonly Dictionary _shpData = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary> _conditionalSet = []; public void Reset() { Clear(); _shpData.Clear(); + _conditionalSet.Clear(); } protected override void Dispose(bool _) @@ -89,21 +107,62 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) + if (identifier.ShapeCondition.Length > 0) { - value = []; - _shpData.Add(identifier.Shape, value); + if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) + { + if (!entry.Value) + return; + + shapes = new Dictionary(); + _conditionalSet.Add(identifier.ShapeCondition, shapes); + } + + Func(shapes); + } + else + { + Func(_shpData); } - value.TrySet(identifier.Slot, identifier.Id, entry); + void Func(Dictionary dict) + { + if (!dict.TryGetValue(identifier.Shape, out var value)) + { + if (!entry.Value) + return; + + value = []; + dict.Add(identifier.Shape, value); + } + + value.TrySet(identifier.Slot, identifier.Id, entry); + } } protected override void RevertModInternal(ShpIdentifier identifier) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) - return; + if (identifier.ShapeCondition.Length > 0) + { + if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) + return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) - _shpData.Remove(identifier.Shape); + Func(shapes); + } + else + { + Func(_shpData); + } + + return; + + void Func(Dictionary dict) + { + if (!_shpData.TryGetValue(identifier.Shape, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + _shpData.Remove(identifier.Shape); + } } } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index c642167f..777be512 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -7,7 +7,7 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape) +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -34,12 +34,39 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return 1; } + var shapeComparison = Shape.CompareTo(other.Shape); + if (shapeComparison is not 0) + return shapeComparison; - return Shape.CompareTo(other.Shape); + return ShapeCondition.CompareTo(other.ShapeCondition); } + public override string ToString() - => $"Shp - {Shape}{(Slot is HumanSlot.Unknown ? " - All Slots & IDs" : $" - {Slot.ToName()}{(Id.HasValue ? $" - {Id.Value.Id}" : " - All IDs")}")}"; + { + var sb = new StringBuilder(64); + sb.Append("Shp - ") + .Append(Shape); + if (Slot is HumanSlot.Unknown) + { + sb.Append(" - All Slots & IDs"); + } + else + { + sb.Append(" - ") + .Append(Slot.ToName()) + .Append(" - "); + if (Id.HasValue) + sb.Append(Id.Value.Id); + else + sb.Append("All IDs"); + } + + if (ShapeCondition.Length > 0) + sb.Append(" - ") + .Append(ShapeCondition); + return sb.ToString(); + } public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { @@ -57,7 +84,24 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Slot is HumanSlot.Unknown && Id is not null) return false; - return ValidateCustomShapeString(Shape); + if (!ValidateCustomShapeString(Shape)) + return false; + + if (ShapeCondition.Length is 0) + return true; + + if (!ValidateCustomShapeString(ShapeCondition)) + return false; + + return Slot switch + { + HumanSlot.Hands when ShapeCondition.IsWrist() => true, + HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true, + HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, + HumanSlot.Feet when ShapeCondition.IsAnkle() => true, + HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, + _ => false, + }; } public static unsafe bool ValidateCustomShapeString(byte* shape) @@ -101,18 +145,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id.HasValue) jObj["Id"] = Id.Value.Id.ToString(); jObj["Shape"] = Shape.ToString(); + if (ShapeCondition.Length > 0) + jObj["ShapeCondition"] = ShapeCondition.ToString(); return jObj; } public static ShpIdentifier? FromJson(JObject jObj) { - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); var shape = jObj["Shape"]?.ToObject(); if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) return null; - var identifier = new ShpIdentifier(slot, id, shapeString); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var shapeCondition = jObj["ShapeCondition"]?.ToObject(); + var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s; + var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index dc3e1a1c..57f6f23f 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,8 +1,10 @@ using System.Reflection.Metadata.Ecma335; using OtterGui.Services; using Penumbra.Collections; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Meta.Manipulations; @@ -30,15 +32,19 @@ public class ShapeManager : IRequiredService, IDisposable private readonly Dictionary[] _temporaryIndices = Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); - private readonly uint[] _temporaryMasks = new uint[NumSlots]; - private readonly uint[] _temporaryValues = new uint[NumSlots]; + private readonly uint[] _temporaryMasks = new uint[NumSlots]; + private readonly uint[] _temporaryValues = new uint[NumSlots]; + private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; public void Dispose() => _attributeHook.Unsubscribe(OnAttributeComputed); private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) { - ComputeCache(model, collection); + if (!collection.HasCache) + return; + + ComputeCache(model, collection.MetaCache!.Shp); for (var i = 0; i < NumSlots; ++i) { if (_temporaryMasks[i] is 0) @@ -52,11 +58,8 @@ public class ShapeManager : IRequiredService, IDisposable } } - private unsafe void ComputeCache(Model human, ModCollection collection) + private unsafe void ComputeCache(Model human, ShpCache cache) { - if (!collection.HasCache) - return; - for (var i = 0; i < NumSlots; ++i) { _temporaryMasks[i] = 0; @@ -68,6 +71,8 @@ public class ShapeManager : IRequiredService, IDisposable if (model is null || model->ModelResourceHandle is null) continue; + _ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set; + ref var shapes = ref model->ModelResourceHandle->Shapes; foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) { @@ -75,8 +80,8 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); - if (collection.MetaCache!.Shp.State.Count > 0 - && collection.MetaCache!.Shp.ShouldBeEnabled(shapeString, modelIndex, human.GetArmorChanged(modelIndex).Set)) + if (cache.State.Count > 0 + && cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) _temporaryValues[i] |= (ushort)(1 << index); } else @@ -86,37 +91,54 @@ public class ShapeManager : IRequiredService, IDisposable } } - UpdateDefaultMasks(); + UpdateDefaultMasks(cache); } - private void UpdateDefaultMasks() + private void UpdateDefaultMasks(ShpCache cache) { foreach (var (shape, topIndex) in _temporaryIndices[1]) { - if (CheckCenter(shape, 'w', 'r') && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) + if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; + CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2); } - if (CheckCenter(shape, 'w', 'a') && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) + if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; + CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3); } } foreach (var (shape, bottomIndex) in _temporaryIndices[3]) { - if (CheckCenter(shape, 'a', 'n') && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) + if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; + CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4); + } + } + + return; + + void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) + { + if (!cache.CheckConditionState(shape, out var dict)) + return; + + foreach (var (subShape, set) in dict) + { + if (set.Contains(slot1, _ids[idx1])) + if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex)) + _temporaryValues[idx1] |= 1u << subIndex; + if (set.Contains(slot2, _ids[idx2])) + if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex)) + _temporaryValues[idx2] |= 1u << subIndex; } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static bool CheckCenter(in ShapeString shape, char first, char second) - => shape.Length > 8 && shape[4] == first && shape[5] == second && shape[6] is (byte)'_'; } diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeString.cs index 5b6f9c52..95ca0933 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeString.cs @@ -37,6 +37,22 @@ public struct ShapeString : IEquatable, IComparable } } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsAnkle() + => CheckCenter('a', 'n'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsWaist() + => CheckCenter('w', 'a'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool IsWrist() + => CheckCenter('w', 'r'); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool CheckCenter(char first, char second) + => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; + public bool Equals(ShapeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 2c99af02..fe7e743c 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,18 +19,20 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; + private ShapeString _conditionBuffer = ShapeString.Empty; private bool _identifierValid; + private bool _conditionValid = true; public override int NumColumns - => 6; + => 7; public override float ColumnHeight => ImUtf8.FrameHeightSpacing; protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty); } protected override void DrawNew() @@ -40,7 +42,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); ImGui.TableNextColumn(); - var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid; var tt = canAdd ? "Stage this edit."u8 : _identifierValid @@ -67,6 +69,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .OrderBy(kvp => kvp.Key.Shape) .ThenBy(kvp => kvp.Key.Slot) .ThenBy(kvp => kvp.Key.Id) + .ThenBy(kvp => kvp.Key.ShapeCondition) .Select(kvp => (kvp.Key, kvp.Value)); protected override int Count @@ -82,6 +85,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + + ImGui.TableNextColumn(); + changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid); return changes; } @@ -101,6 +107,13 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); + + ImGui.TableNextColumn(); + if (identifier.ShapeCondition.Length > 0) + { + ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor); + ImUtf8.HoverTooltip("Connector condition for this shape to be activated."); + } } private static bool DrawEntry(ref ShpEntry entry, bool disabled) @@ -154,7 +167,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -168,13 +181,37 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ret = true; if (slot is HumanSlot.Unknown) + { identifier = identifier with { Id = null, Slot = slot, }; + } else - identifier = identifier with { Slot = slot }; + { + if (_conditionBuffer.Length > 0 + && (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs + || _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body + || _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs)) + { + identifier = identifier with + { + Slot = slot, + ShapeCondition = ShapeString.Empty, + }; + _conditionValid = false; + } + else + { + identifier = identifier with + { + Slot = slot, + ShapeCondition = _conditionBuffer, + }; + _conditionValid = true; + } + } } } @@ -204,6 +241,33 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } + public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, + float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = ShpIdentifier.ValidateCustomShapeString(buffer) + && (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs + || buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs + || buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands); + if (valid) + identifier = identifier with { ShapeCondition = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip( + "Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8); + return ret; + } + private static ReadOnlySpan AvailableSlots => [ diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 5292bd17..8439587c 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; @@ -12,9 +13,9 @@ namespace Penumbra.UI.Tabs.Debug; public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) : IUiService { - private int _objectIndex = 0; + private int _objectIndex; - public unsafe void Draw() + public void Draw() { ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); var actor = objects[0]; @@ -31,93 +32,117 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) return; } - var data = resolver.IdentifyCollection(actor.AsObject, true); - using (var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})")) + DrawCollectionShapeCache(actor); + DrawCharacterShapes(human); + } + + private unsafe void DrawCollectionShapeCache(Actor actor) + { + var data = resolver.IdentifyCollection(actor.AsObject, true); + using var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})"); + if (!treeNode1.Success || !data.ModCollection.HasCache) + return; + + using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) { - if (treeNode1.Success && data.ModCollection.HasCache) - { - using var table = ImUtf8.Table("##cacheTable"u8, 2, ImGuiTableFlags.RowBg); - if (!table) - return; - - ImUtf8.TableSetupColumn("shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("enabled"u8, ImGuiTableColumnFlags.WidthStretch); - - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) - { - ImUtf8.DrawTableColumn(shape.AsSpan); - if (set.All) - { - ImUtf8.DrawTableColumn("All"u8); - } - else - { - ImGui.TableNextColumn(); - foreach (var slot in ShapeManager.UsedModels) - { - if (!set[slot]) - continue; - - ImUtf8.Text($"All {slot.ToName()}, "); - ImGui.SameLine(0, 0); - } - - foreach (var item in set.Where(i => !set[i.Slot])) - { - ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); - ImGui.SameLine(0, 0); - } - } - } - } + ImGui.TableNextColumn(); + DrawShape(shape, set); } - using (var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8)) + foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState) { - if (treeNode2) + foreach (var (shape, set) in dict) { - using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); - if (!table) - return; + ImUtf8.DrawTableColumn(condition.AsSpan); + DrawShape(shape, set); + } + } + } - ImUtf8.TableSetupColumn("idx"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("name"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("ptr"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); - ImUtf8.TableSetupColumn("mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("shapes"u8, ImGuiTableColumnFlags.WidthStretch); + private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set) + { + ImUtf8.DrawTableColumn(shape.AsSpan); + if (set.All) + { + ImUtf8.DrawTableColumn("All"u8); + } + else + { + ImGui.TableNextColumn(); + foreach (var slot in ShapeManager.UsedModels) + { + if (!set[slot]) + continue; - var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); - for (var i = 0; i < human.AsHuman->SlotCount; ++i) + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var item in set.Where(i => !set[i.Slot])) + { + ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + } + } + + private unsafe void DrawCharacterShapes(Model human) + { + using var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8); + if (!treeNode2) + return; + + using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledShapeKeyIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) { - ImUtf8.DrawTableColumn($"{(uint)i:D2}"); - ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); - - ImGui.TableNextColumn(); - var model = human.AsHuman->Models[i]; - Penumbra.Dynamis.DrawPointer((nint)model); - if (model is not null) - { - var mask = model->EnabledShapeKeyIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) - { - var disabled = (mask & (1u << idx)) is 0; - using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); - ImUtf8.Text(shape.AsSpan()); - ImGui.SameLine(0, 0); - ImUtf8.Text(", "u8); - if (idx % 8 < 7) - ImGui.SameLine(0, 0); - } - } - else - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - } + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(shape.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); } } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } } } } diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 197f3104..4f868a0a 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -16,6 +16,12 @@ "minLength": 5, "maxLength": 30, "pattern": "^shpx_" + }, + "ShapeCondition": { + "type": "string", + "minLength": 8, + "maxLength": 30, + "pattern": "^shpx_(wa|an|wr)_" } }, "required": [ From 08e8b9d2a460ad7245ddcf6006e10d23cfc3f267 Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 15 May 2025 22:28:36 +0000 Subject: [PATCH 691/865] [CI] Updating repo.json for testing_1.3.6.10 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 13d91e5c..137ba00c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.9", + "TestingAssemblyVersion": "1.3.6.10", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.10/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 52927ff06bbc7c159a237876e4cf50ea328dbddd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 12:31:13 +0200 Subject: [PATCH 692/865] Fix clipping in meta edits. --- OtterGui | 2 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/OtterGui b/OtterGui index f130c928..9aeda9a8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98 +Subproject commit 9aeda9a892d9b971e32b10db21a8daf9c0b9ee53 diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index a6f042b7..7e788462 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -44,9 +44,13 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta DrawNew(); var height = ColumnHeight; - var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY()); - var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); - ImGuiClip.DrawEndDummy(remainder, height); + var skips = ImGuiClip.GetNecessarySkipsAtPos(height, ImGui.GetCursorPosY(), Count); + if (skips < Count) + { + var remainder = ImGuiClip.ClippedTableDraw(Enumerate(), skips, DrawLine, Count); + if (remainder > 0) + ImGuiClip.DrawEndDummy(remainder, height); + } void DrawLine((TIdentifier Identifier, TEntry Value) pair) => DrawEntry(pair.Identifier, pair.Value); From 3078c467d0666e28ab25dd31d515648300f946da Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 12:31:34 +0200 Subject: [PATCH 693/865] Fix issue with empty and temporary settings. --- Penumbra/Mods/Settings/ModSettings.cs | 15 +++++++++++---- Penumbra/Mods/Settings/TemporaryModSettings.cs | 11 +++++++++-- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 4 ++-- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 07217d4d..bbdd6bfa 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -11,11 +11,18 @@ namespace Penumbra.Mods.Settings; /// Contains the settings for a given mod. public class ModSettings { - public static readonly ModSettings Empty = new(); + public static readonly ModSettings Empty = new(true); - public SettingList Settings { get; internal init; } = []; - public ModPriority Priority { get; set; } - public bool Enabled { get; set; } + public SettingList Settings { get; internal init; } = []; + public ModPriority Priority { get; set; } + public bool Enabled { get; set; } + public bool IsEmpty { get; protected init; } + + public ModSettings() + { } + + protected ModSettings(bool empty) + => IsEmpty = empty; // Create an independent copy of the current settings. public ModSettings DeepCopy() diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index a16a9feb..ce438aac 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -2,9 +2,11 @@ namespace Penumbra.Mods.Settings; public sealed class TemporaryModSettings : ModSettings { + public new static readonly TemporaryModSettings Empty = new(true); + public const string OwnSource = "yourself"; public string Source = string.Empty; - public int Lock = 0; + public int Lock; public bool ForceInherit; // Create default settings for a given mod. @@ -21,12 +23,16 @@ public sealed class TemporaryModSettings : ModSettings public TemporaryModSettings() { } + private TemporaryModSettings(bool empty) + : base(empty) + { } + public TemporaryModSettings(Mod mod, ModSettings? clone, string source = OwnSource, int key = 0) { Source = source; Lock = key; ForceInherit = clone == null; - if (clone != null && clone != Empty) + if (clone is { IsEmpty: false }) { Enabled = clone.Enabled; Priority = clone.Priority; @@ -34,6 +40,7 @@ public sealed class TemporaryModSettings : ModSettings } else { + IsEmpty = true; Enabled = false; Priority = ModPriority.Default; Settings = SettingList.Default(mod); diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 666fce61..3e165cb5 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -49,7 +49,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle case GroupDrawBehaviour.SingleSelection: ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; - DrawSingleGroupCombo(group, idx, settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]); + DrawSingleGroupCombo(group, idx, settings.IsEmpty ? group.DefaultSettings : settings.Settings[idx]); break; } } @@ -59,7 +59,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle { ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; - var option = settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]; + var option = settings.IsEmpty ? group.DefaultSettings : settings.Settings[idx]; if (group.Behaviour is GroupDrawBehaviour.MultiSelection) DrawMultiGroup(group, idx, option); else diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 3988de35..7c6ebf74 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -183,7 +183,7 @@ public class ModPanelSettingsTab( /// private void DrawRemoveSettings() { - var drawInherited = !_inherited && selection.Settings != ModSettings.Empty; + var drawInherited = !_inherited && !selection.Settings.IsEmpty; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X : 0; var buttonSize = ImUtf8.CalcTextSize("Turn Permanent_"u8).X; var offset = drawInherited From fbc4c2d054dd37575d495736d01fa6cd97b7ec07 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 12:54:09 +0200 Subject: [PATCH 694/865] Improve option select combo. --- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 75 +++++++++++++------ .../UI/AdvancedWindow/OptionSelectCombo.cs | 43 +++++++++++ 2 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index ccbbf0db..0b9fcde9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -7,9 +7,11 @@ using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui; using OtterGui.Extensions; +using OtterGui.Log; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -34,6 +36,41 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; +public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) + : FilterComboCache<(string FullName, (int Group, int Data) Index)>( + () => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log) +{ + private ImRaii.ColorStyle _border; + + protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, + ImGuiComboFlags flags) + { + _border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value()); + base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); + _border.Dispose(); + } + + protected override void DrawFilter(int currentSelected, float width) + { + _border.Dispose(); + base.DrawFilter(currentSelected, width); + } + + public bool Draw(float width) + { + var flags = window.Mod!.AllDataContainers.Count() switch + { + 0 => ImGuiComboFlags.NoArrowButton, + > 8 => ImGuiComboFlags.HeightLargest, + _ => ImGuiComboFlags.None, + }; + return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + => ImUtf8.Selectable(Items[globalIdx].FullName, selected); +} + public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; @@ -49,11 +86,12 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private readonly IDragDropManager _dragDropManager; private readonly IDataManager _gameData; private readonly IFramework _framework; + private readonly OptionSelectCombo _optionSelect; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; - public Mod? Mod { get; private set; } + public Mod? Mod { get; private set; } public bool IsLoading @@ -208,7 +246,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService if (IsLoading) { var radius = 100 * ImUtf8.GlobalScale; - var thickness = (int) (20 * ImUtf8.GlobalScale); + var thickness = (int)(20 * ImUtf8.GlobalScale); var offsetX = ImGui.GetContentRegionAvail().X / 2 - radius; var offsetY = ImGui.GetContentRegionAvail().Y / 2 - radius; ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(offsetX, offsetY)); @@ -216,7 +254,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService return; } - using var tabBar = ImRaii.TabBar("##tabs"); + using var tabBar = ImUtf8.TabBar("##tabs"u8); if (!tabBar) return; @@ -231,7 +269,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _materialTab.Draw(); DrawTextureTab(); _shaderPackageTab.Draw(); - using (var tab = ImRaii.TabItem("Item Swap")) + using (var tab = ImUtf8.TabItem("Item Swap"u8)) { if (tab) _itemSwapTab.DrawContent(); @@ -453,10 +491,11 @@ public partial class ModEditWindow : Window, IDisposable, IUiService private bool DrawOptionSelectHeader() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); - var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); - var ret = false; - if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, _editor.Option is DefaultSubMod)) + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); + var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); + var ret = false; + if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, + _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0).Wait(); ret = true; @@ -470,22 +509,11 @@ public partial class ModEditWindow : Window, IDisposable, IUiService } ImGui.SameLine(); - ImGui.SetNextItemWidth(width.X); - style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); - using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImUtf8.Combo("##optionSelector"u8, _editor.Option!.GetFullName()); - if (!combo) - return ret; - - foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex()) + if (_optionSelect.Draw(width.X)) { - using var id = ImRaii.PushId(idx); - if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) - { - var (groupIdx, dataIdx) = option.GetDataIndices(); - _editor.LoadOption(groupIdx, dataIdx).Wait(); - ret = true; - } + var (groupIdx, dataIdx) = _optionSelect.CurrentSelection.Index; + _editor.LoadOption(groupIdx, dataIdx).Wait(); + ret = true; } return ret; @@ -656,6 +684,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _fileDialog = fileDialog; _framework = framework; _metaDrawers = metaDrawers; + _optionSelect = new OptionSelectCombo(editor, this); _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => mtrlTabFactory.Create(this, new MtrlFile(bytes), path, writable)); diff --git a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs new file mode 100644 index 00000000..1fa12b6d --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs @@ -0,0 +1,43 @@ +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Text; +using OtterGui.Widgets; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) + : FilterComboCache<(string FullName, (int Group, int Data) Index)>( + () => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log) +{ + private ImRaii.ColorStyle _border; + + protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, + ImGuiComboFlags flags) + { + _border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value()); + base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); + _border.Dispose(); + } + + protected override void DrawFilter(int currentSelected, float width) + { + _border.Dispose(); + base.DrawFilter(currentSelected, width); + } + + public bool Draw(float width) + { + var flags = window.Mod!.AllDataContainers.Count() switch + { + 0 => ImGuiComboFlags.NoArrowButton, + > 8 => ImGuiComboFlags.HeightLargest, + _ => ImGuiComboFlags.None, + }; + return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags); + } + + protected override bool DrawSelectable(int globalIdx, bool selected) + => ImUtf8.Selectable(Items[globalIdx].FullName, selected); +} From e326e3d809b0cfa7a71f9290e6e449f8e05e0f46 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 15:52:47 +0200 Subject: [PATCH 695/865] Update shp conditions. --- Penumbra/Collections/Cache/ShpCache.cs | 81 ++++++++--------- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 61 +++++++------ Penumbra/Meta/ShapeManager.cs | 26 +++--- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 89 +++++++++---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 35 -------- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 13 +-- schemas/structs/meta_enums.json | 4 + schemas/structs/meta_shp.json | 7 +- 8 files changed, 137 insertions(+), 179 deletions(-) diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index eaf949d9..ee6a4e65 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -8,27 +8,24 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) - => _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); - internal IReadOnlyDictionary State - => _shpData; - - internal IEnumerable<(ShapeString, IReadOnlyDictionary)> ConditionState - => _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary)kvp.Value)); - - public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary? dict) - { - if (_conditionalSet.TryGetValue(condition, out var d)) + internal IReadOnlyDictionary State(ShapeConnectorCondition connector) + => connector switch { - dict = d; - return true; - } + ShapeConnectorCondition.None => _shpData, + ShapeConnectorCondition.Wrists => _wristConnectors, + ShapeConnectorCondition.Waist => _waistConnectors, + ShapeConnectorCondition.Ankles => _ankleConnectors, + _ => [], + }; - dict = null; - return false; - } + public int EnabledCount { get; private set; } + public bool ShouldBeEnabled(ShapeConnectorCondition connector, in ShapeString shape, HumanSlot slot, PrimaryId id) + => State(connector).TryGetValue(shape, out var value) && value.Contains(slot, id); + public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -92,14 +89,18 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) => !_allIds.HasAnySet() && Count is 0; } - private readonly Dictionary _shpData = []; - private readonly Dictionary> _conditionalSet = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary _wristConnectors = []; + private readonly Dictionary _waistConnectors = []; + private readonly Dictionary _ankleConnectors = []; public void Reset() { Clear(); _shpData.Clear(); - _conditionalSet.Clear(); + _wristConnectors.Clear(); + _waistConnectors.Clear(); + _ankleConnectors.Clear(); } protected override void Dispose(bool _) @@ -107,24 +108,16 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { - if (identifier.ShapeCondition.Length > 0) + switch (identifier.ConnectorCondition) { - if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) - { - if (!entry.Value) - return; - - shapes = new Dictionary(); - _conditionalSet.Add(identifier.ShapeCondition, shapes); - } - - Func(shapes); - } - else - { - Func(_shpData); + case ShapeConnectorCondition.None: Func(_shpData); break; + case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break; + case ShapeConnectorCondition.Waist: Func(_waistConnectors); break; + case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break; } + return; + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) @@ -136,22 +129,19 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) dict.Add(identifier.Shape, value); } - value.TrySet(identifier.Slot, identifier.Id, entry); + if (value.TrySet(identifier.Slot, identifier.Id, entry)) + ++EnabledCount; } } protected override void RevertModInternal(ShpIdentifier identifier) { - if (identifier.ShapeCondition.Length > 0) + switch (identifier.ConnectorCondition) { - if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes)) - return; - - Func(shapes); - } - else - { - Func(_shpData); + case ShapeConnectorCondition.None: Func(_shpData); break; + case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break; + case ShapeConnectorCondition.Waist: Func(_waistConnectors); break; + case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break; } return; @@ -162,7 +152,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + { + --EnabledCount; _shpData.Remove(identifier.Shape); + } } } } diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index 777be512..b3fdb0cb 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -7,7 +8,16 @@ using Penumbra.Interop.Structs; namespace Penumbra.Meta.Manipulations; -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition) +[JsonConverter(typeof(StringEnumConverter))] +public enum ShapeConnectorCondition : byte +{ + None = 0, + Wrists = 1, + Waist = 2, + Ankles = 3, +} + +public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -34,11 +44,11 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape return 1; } - var shapeComparison = Shape.CompareTo(other.Shape); - if (shapeComparison is not 0) - return shapeComparison; + var conditionComparison = ConnectorCondition.CompareTo(other.ConnectorCondition); + if (conditionComparison is not 0) + return conditionComparison; - return ShapeCondition.CompareTo(other.ShapeCondition); + return Shape.CompareTo(other.Shape); } @@ -62,9 +72,13 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape sb.Append("All IDs"); } - if (ShapeCondition.Length > 0) - sb.Append(" - ") - .Append(ShapeCondition); + switch (ConnectorCondition) + { + case ShapeConnectorCondition.Wrists: sb.Append(" - Wrist Connector"); break; + case ShapeConnectorCondition.Waist: sb.Append(" - Waist Connector"); break; + case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break; + } + return sb.ToString(); } @@ -87,20 +101,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (!ValidateCustomShapeString(Shape)) return false; - if (ShapeCondition.Length is 0) - return true; - - if (!ValidateCustomShapeString(ShapeCondition)) + if (!Enum.IsDefined(ConnectorCondition)) return false; - return Slot switch + return ConnectorCondition switch { - HumanSlot.Hands when ShapeCondition.IsWrist() => true, - HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true, - HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, - HumanSlot.Feet when ShapeCondition.IsAnkle() => true, - HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true, - _ => false, + ShapeConnectorCondition.None => true, + ShapeConnectorCondition.Wrists => Slot is HumanSlot.Body or HumanSlot.Hands or HumanSlot.Unknown, + ShapeConnectorCondition.Waist => Slot is HumanSlot.Body or HumanSlot.Legs or HumanSlot.Unknown, + ShapeConnectorCondition.Ankles => Slot is HumanSlot.Legs or HumanSlot.Feet or HumanSlot.Unknown, + _ => false, }; } @@ -145,8 +155,8 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id.HasValue) jObj["Id"] = Id.Value.Id.ToString(); jObj["Shape"] = Shape.ToString(); - if (ShapeCondition.Length > 0) - jObj["ShapeCondition"] = ShapeCondition.ToString(); + if (ConnectorCondition is not ShapeConnectorCondition.None) + jObj["ConnectorCondition"] = ConnectorCondition.ToString(); return jObj; } @@ -156,11 +166,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) return null; - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); - var shapeCondition = jObj["ShapeCondition"]?.ToObject(); - var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s; - var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; + var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index 57f6f23f..7431b1c2 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -1,4 +1,3 @@ -using System.Reflection.Metadata.Ecma335; using OtterGui.Services; using Penumbra.Collections; using Penumbra.Collections.Cache; @@ -80,8 +79,7 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryIndices[i].TryAdd(shapeString, index); _temporaryMasks[i] |= (ushort)(1 << index); - if (cache.State.Count > 0 - && cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) + if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) _temporaryValues[i] |= (ushort)(1 << index); } else @@ -102,14 +100,14 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[2] |= 1u << handIndex; - CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2); + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); } if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) { _temporaryValues[1] |= 1u << topIndex; _temporaryValues[3] |= 1u << legIndex; - CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3); + CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); } } @@ -119,25 +117,23 @@ public class ShapeManager : IRequiredService, IDisposable { _temporaryValues[3] |= 1u << bottomIndex; _temporaryValues[4] |= 1u << footIndex; - CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4); + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); } } return; - void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) + void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) { - if (!cache.CheckConditionState(shape, out var dict)) + if (dict.Count is 0) return; - foreach (var (subShape, set) in dict) + foreach (var (shape, set) in dict) { - if (set.Contains(slot1, _ids[idx1])) - if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex)) - _temporaryValues[idx1] |= 1u << subIndex; - if (set.Contains(slot2, _ids[idx2])) - if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex)) - _temporaryValues[idx2] |= 1u << subIndex; + if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1)) + _temporaryValues[idx1] |= 1u << index1; + if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2)) + _temporaryValues[idx2] |= 1u << index2; } } } diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index fe7e743c..c40726f8 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -19,10 +19,8 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; - private ShapeString _conditionBuffer = ShapeString.Empty; + private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; private bool _identifierValid; - private bool _conditionValid = true; public override int NumColumns => 7; @@ -32,7 +30,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeString.Empty); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None); } protected override void DrawNew() @@ -42,7 +40,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile new Lazy(() => MetaDictionary.SerializeTo([], Editor.Shp))); ImGui.TableNextColumn(); - var canAdd = !Editor.Contains(Identifier) && _identifierValid && _conditionValid; + var canAdd = !Editor.Contains(Identifier) && _identifierValid; var tt = canAdd ? "Stage this edit."u8 : _identifierValid @@ -69,7 +67,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile .OrderBy(kvp => kvp.Key.Shape) .ThenBy(kvp => kvp.Key.Slot) .ThenBy(kvp => kvp.Key.Id) - .ThenBy(kvp => kvp.Key.ShapeCondition) + .ThenBy(kvp => kvp.Key.ConnectorCondition) .Select(kvp => (kvp.Key, kvp.Value)); protected override int Count @@ -87,7 +85,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile changes |= DrawShapeKeyInput(ref identifier, ref _buffer, ref _identifierValid); ImGui.TableNextColumn(); - changes |= DrawShapeConditionInput(ref identifier, ref _conditionBuffer, ref _conditionValid); + changes |= DrawConnectorConditionInput(ref identifier); return changes; } @@ -109,9 +107,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(identifier.Shape.AsSpan, FrameColor); ImGui.TableNextColumn(); - if (identifier.ShapeCondition.Length > 0) + if (identifier.ConnectorCondition is not ShapeConnectorCondition.None) { - ImUtf8.TextFramed(identifier.ShapeCondition.AsSpan, FrameColor); + ImUtf8.TextFramed($"{identifier.ConnectorCondition}", FrameColor); ImUtf8.HoverTooltip("Connector condition for this shape to be activated."); } } @@ -190,27 +188,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } else { - if (_conditionBuffer.Length > 0 - && (_conditionBuffer.IsAnkle() && slot is not HumanSlot.Feet and not HumanSlot.Legs - || _conditionBuffer.IsWrist() && slot is not HumanSlot.Hands and not HumanSlot.Body - || _conditionBuffer.IsWaist() && slot is not HumanSlot.Body and not HumanSlot.Legs)) + identifier = identifier with { - identifier = identifier with + Slot = slot, + ConnectorCondition = Identifier.ConnectorCondition switch { - Slot = slot, - ShapeCondition = ShapeString.Empty, - }; - _conditionValid = false; - } - else - { - identifier = identifier with - { - Slot = slot, - ShapeCondition = _conditionBuffer, - }; - _conditionValid = true; - } + ShapeConnectorCondition.Wrists when slot is HumanSlot.Body or HumanSlot.Hands => ShapeConnectorCondition.Wrists, + ShapeConnectorCondition.Waist when slot is HumanSlot.Body or HumanSlot.Legs => ShapeConnectorCondition.Waist, + ShapeConnectorCondition.Ankles when slot is HumanSlot.Legs or HumanSlot.Feet => ShapeConnectorCondition.Ankles, + _ => ShapeConnectorCondition.None, + }, + }; + ret = true; } } } @@ -241,30 +230,40 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawShapeConditionInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, - float unscaledWidth = 150) + public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 150) { - var ret = false; - var ptr = Unsafe.AsPointer(ref buffer); - var span = new Span(ptr, ShapeString.MaxLength + 1); - using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + var (showWrists, showWaist, showAnkles, disable) = identifier.Slot switch { - ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); - if (ImUtf8.InputText("##shpCondition"u8, span, out int newLength, "Shape Condition..."u8)) + HumanSlot.Unknown => (true, true, true, false), + HumanSlot.Body => (true, true, false, false), + HumanSlot.Legs => (false, true, true, false), + HumanSlot.Hands => (true, false, false, false), + HumanSlot.Feet => (false, false, true, false), + _ => (false, false, false, true), + }; + using var disabled = ImRaii.Disabled(disable); + using (var combo = ImUtf8.Combo("##shpCondition"u8, $"{identifier.ConnectorCondition}")) + { + if (combo) { - buffer.ForceLength((byte)newLength); - valid = ShpIdentifier.ValidateCustomShapeString(buffer) - && (buffer.IsAnkle() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Feet or HumanSlot.Legs - || buffer.IsWaist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Legs - || buffer.IsWrist() && identifier.Slot is HumanSlot.Unknown or HumanSlot.Body or HumanSlot.Hands); - if (valid) - identifier = identifier with { ShapeCondition = buffer }; - ret = true; + if (ImUtf8.Selectable("None"u8, identifier.ConnectorCondition is ShapeConnectorCondition.None)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.None }; + + if (showWrists && ImUtf8.Selectable("Wrists"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Wrists)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Wrists }; + + if (showWaist && ImUtf8.Selectable("Waist"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Waist)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Waist }; + + if (showAnkles && ImUtf8.Selectable("Ankles"u8, identifier.ConnectorCondition is ShapeConnectorCondition.Ankles)) + identifier = identifier with { ConnectorCondition = ShapeConnectorCondition.Ankles }; } } ImUtf8.HoverTooltip( - "Supported conditional shape keys need to have the format `shpx_an_*` (Legs or Feet), `shpx_wr_*` (Body or Hands), or `shpx_wa_*` (Body or Legs) and a maximum length of 30 characters."u8); + "Only activate this shape key if any custom connector shape keys (shpx_[wr|wa|an]_*) are also enabled through matching attributes."u8); return ret; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 0b9fcde9..e148167b 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -36,41 +36,6 @@ using MdlMaterialEditor = Penumbra.Mods.Editor.MdlMaterialEditor; namespace Penumbra.UI.AdvancedWindow; -public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) - : FilterComboCache<(string FullName, (int Group, int Data) Index)>( - () => window.Mod!.AllDataContainers.Select(c => (c.GetFullName(), c.GetDataIndices())).ToList(), MouseWheelType.Control, Penumbra.Log) -{ - private ImRaii.ColorStyle _border; - - protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, - ImGuiComboFlags flags) - { - _border = ImRaii.PushFrameBorder(ImUtf8.GlobalScale, ColorId.FolderLine.Value()); - base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); - _border.Dispose(); - } - - protected override void DrawFilter(int currentSelected, float width) - { - _border.Dispose(); - base.DrawFilter(currentSelected, width); - } - - public bool Draw(float width) - { - var flags = window.Mod!.AllDataContainers.Count() switch - { - 0 => ImGuiComboFlags.NoArrowButton, - > 8 => ImGuiComboFlags.HeightLargest, - _ => ImGuiComboFlags.None, - }; - return Draw("##optionSelector", editor.Option!.GetFullName(), string.Empty, width, ImGui.GetTextLineHeight(), flags); - } - - protected override bool DrawSelectable(int globalIdx, bool selected) - => ImUtf8.Selectable(Items[globalIdx].FullName, selected); -} - public partial class ModEditWindow : Window, IDisposable, IUiService { private const string WindowBaseLabel = "###SubModEdit"; diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 8439587c..109cb5c4 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -8,6 +8,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.Interop.PathResolving; using Penumbra.Meta; +using Penumbra.Meta.Manipulations; namespace Penumbra.UI.Tabs.Debug; @@ -52,17 +53,11 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State) + foreach (var condition in Enum.GetValues()) { - ImGui.TableNextColumn(); - DrawShape(shape, set); - } - - foreach (var (condition, dict) in data.ModCollection.MetaCache!.Shp.ConditionState) - { - foreach (var (shape, set) in dict) + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) { - ImUtf8.DrawTableColumn(condition.AsSpan); + ImUtf8.DrawTableColumn(condition.ToString()); DrawShape(shape, set); } } diff --git a/schemas/structs/meta_enums.json b/schemas/structs/meta_enums.json index 2fc65a0d..bad184e0 100644 --- a/schemas/structs/meta_enums.json +++ b/schemas/structs/meta_enums.json @@ -29,6 +29,10 @@ "$anchor": "SubRace", "enum": [ "Unknown", "Midlander", "Highlander", "Wildwood", "Duskwight", "Plainsfolk", "Dunesfolk", "SeekerOfTheSun", "KeeperOfTheMoon", "Seawolf", "Hellsguard", "Raen", "Xaela", "Helion", "Lost", "Rava", "Veena" ] }, + "ShapeConnectorCondition": { + "$anchor": "ShapeConnectorCondition", + "enum": [ "None", "Wrists", "Waist", "Ankles" ] + }, "U8": { "$anchor": "U8", "oneOf": [ diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 4f868a0a..851842a4 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -17,11 +17,8 @@ "maxLength": 30, "pattern": "^shpx_" }, - "ShapeCondition": { - "type": "string", - "minLength": 8, - "maxLength": 30, - "pattern": "^shpx_(wa|an|wr)_" + "ConnectorCondition": { + "$ref": "meta_enums.json#ShapeConnectorCondition" } }, "required": [ From 6e4e28fa00f73d5c937e8cd12553917af79bf798 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 16:00:09 +0200 Subject: [PATCH 696/865] Fix disabling conditional shapes. --- Penumbra/Collections/Cache/ShpCache.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index ee6a4e65..2fe7f933 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -22,10 +22,6 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) public int EnabledCount { get; private set; } - - public bool ShouldBeEnabled(ShapeConnectorCondition connector, in ShapeString shape, HumanSlot slot, PrimaryId id) - => State(connector).TryGetValue(shape, out var value) && value.Contains(slot, id); - public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> { private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); @@ -148,13 +144,14 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) void Func(Dictionary dict) { - if (!_shpData.TryGetValue(identifier.Shape, out var value)) + if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty) + if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False)) { --EnabledCount; - _shpData.Remove(identifier.Shape); + if (value.IsEmpty) + dict.Remove(identifier.Shape); } } } From e18e4bb0e1fb0dcc450736c100cd3f2867d811aa Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 18 May 2025 14:02:16 +0000 Subject: [PATCH 697/865] [CI] Updating repo.json for testing_1.3.6.11 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 137ba00c..c077c355 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.10", + "TestingAssemblyVersion": "1.3.6.11", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.10/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.11/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 47b589540435c8f81826887de33c9827897b7f3d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 18 May 2025 22:00:10 +0200 Subject: [PATCH 698/865] Fix issue with temp settings again. --- Penumbra/Mods/Settings/TemporaryModSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index ce438aac..d3e36ef6 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -40,7 +40,6 @@ public sealed class TemporaryModSettings : ModSettings } else { - IsEmpty = true; Enabled = false; Priority = ModPriority.Default; Settings = SettingList.Default(mod); From 68b68d6ce7657d32466ad8a8622f203a43936887 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 19 May 2025 17:15:29 +0200 Subject: [PATCH 699/865] Fix some issues with customization IDs and supported counts. --- Penumbra.GameData | 2 +- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 7 +++++++ Penumbra/Meta/ShapeManager.cs | 2 +- Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 10 +++++++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 8e57c2e1..b15c0f07 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3 +Subproject commit b15c0f07ba270a7b6a350411006e003da9818d1b diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index b3fdb0cb..3be46d32 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; namespace Penumbra.Meta.Manipulations; @@ -98,6 +99,12 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Slot is HumanSlot.Unknown && Id is not null) return false; + if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue }) + return false; + + if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) + return false; + if (!ValidateCustomShapeString(Shape)) return false; diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs index 7431b1c2..abd4c3b8 100644 --- a/Penumbra/Meta/ShapeManager.cs +++ b/Penumbra/Meta/ShapeManager.cs @@ -70,7 +70,7 @@ public class ShapeManager : IRequiredService, IDisposable if (model is null || model->ModelResourceHandle is null) continue; - _ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set; + _ids[(int)modelIndex] = human.GetModelId(modelIndex); ref var shapes = ref model->ModelResourceHandle->Shapes; foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index c40726f8..6505ecc0 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -5,6 +5,7 @@ using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; using Penumbra.Meta; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -151,9 +152,8 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile } else { - if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, - ExpandedEqpGmpBase.Count - 1, - false)) + var max = identifier.Slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1; + if (IdInput("##shpPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, max, false)) { identifier = identifier with { Id = setId }; ret = true; @@ -190,6 +190,10 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile { identifier = identifier with { + Id = identifier.Id.HasValue + ? (PrimaryId)Math.Clamp(identifier.Id.Value.Id, 0, + slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1) + : null, Slot = slot, ConnectorCondition = Identifier.ConnectorCondition switch { From fefa3852f7755e42e63957d9e5a244af9dc8001a Mon Sep 17 00:00:00 2001 From: Actions User Date: Mon, 19 May 2025 15:17:54 +0000 Subject: [PATCH 700/865] [CI] Updating repo.json for testing_1.3.6.12 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index c077c355..0413b876 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.11", + "TestingAssemblyVersion": "1.3.6.12", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.12/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 861cbc77590e196a3c1f9fb828aebb3432696ec1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 May 2025 17:05:49 +0200 Subject: [PATCH 701/865] Add global EQP edits to always hide horns or ears. --- Penumbra/Collections/Cache/GlobalEqpCache.cs | 27 ++++++++++++++++++- .../Manipulations/GlobalEqpManipulation.cs | 12 ++++++--- Penumbra/Meta/Manipulations/GlobalEqpType.cs | 14 +++++++++- schemas/structs/meta_geqp.json | 6 ++--- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Penumbra/Collections/Cache/GlobalEqpCache.cs b/Penumbra/Collections/Cache/GlobalEqpCache.cs index 60e782b5..7d2fbf64 100644 --- a/Penumbra/Collections/Cache/GlobalEqpCache.cs +++ b/Penumbra/Collections/Cache/GlobalEqpCache.cs @@ -15,6 +15,9 @@ public class GlobalEqpCache : ReadWriteDictionary, private readonly HashSet _doNotHideRingR = []; private bool _doNotHideVieraHats; private bool _doNotHideHrothgarHats; + private bool _hideAuRaHorns; + private bool _hideVieraEars; + private bool _hideMiqoteEars; public new void Clear() { @@ -26,6 +29,9 @@ public class GlobalEqpCache : ReadWriteDictionary, _doNotHideRingR.Clear(); _doNotHideHrothgarHats = false; _doNotHideVieraHats = false; + _hideAuRaHorns = false; + _hideVieraEars = false; + _hideMiqoteEars = false; } public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor) @@ -39,8 +45,20 @@ public class GlobalEqpCache : ReadWriteDictionary, if (_doNotHideHrothgarHats) original |= EqpEntry.HeadShowHrothgarHat; + if (_hideAuRaHorns) + original &= ~EqpEntry.HeadShowEarAuRa; + + if (_hideVieraEars) + original &= ~EqpEntry.HeadShowEarViera; + + if (_hideMiqoteEars) + original &= ~EqpEntry.HeadShowEarMiqote; + if (_doNotHideEarrings.Contains(armor[5].Set)) - original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura; + original |= EqpEntry.HeadShowEarringsHyurRoe + | EqpEntry.HeadShowEarringsLalaElezen + | EqpEntry.HeadShowEarringsMiqoHrothViera + | EqpEntry.HeadShowEarringsAura; if (_doNotHideNecklace.Contains(armor[6].Set)) original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace; @@ -53,6 +71,7 @@ public class GlobalEqpCache : ReadWriteDictionary, if (_doNotHideRingL.Contains(armor[9].Set)) original |= EqpEntry.HandShowRingL; + return original; } @@ -71,6 +90,9 @@ public class GlobalEqpCache : ReadWriteDictionary, GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true), GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true), + GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true), + GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true), + GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true), _ => false, }; return true; @@ -90,6 +112,9 @@ public class GlobalEqpCache : ReadWriteDictionary, GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition), GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false), GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false), + GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false), + GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false), + GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false), _ => false, }; return true; diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 1365d9d3..33399a36 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -16,10 +16,10 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier if (!Enum.IsDefined(Type)) return false; - if (Type is GlobalEqpType.DoNotHideVieraHats or GlobalEqpType.DoNotHideHrothgarHats) - return Condition == 0; + if (Type.HasCondition()) + return Condition.Id is not 0; - return Condition != 0; + return Condition.Id is 0; } public JObject AddToJson(JObject jObj) @@ -89,6 +89,12 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName()); else if (Type is GlobalEqpType.DoNotHideHrothgarHats) changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName()); + else if (Type is GlobalEqpType.HideHorns) + changedItems.UpdateCountOrSet("All Au Ra Horns", () => new IdentifiedName()); + else if (Type is GlobalEqpType.HideVieraEars) + changedItems.UpdateCountOrSet("All Viera Ears", () => new IdentifiedName()); + else if (Type is GlobalEqpType.HideMiqoteEars) + changedItems.UpdateCountOrSet("All Miqo'te Ears", () => new IdentifiedName()); } public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/GlobalEqpType.cs b/Penumbra/Meta/Manipulations/GlobalEqpType.cs index 1a7396f9..29bfe825 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpType.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpType.cs @@ -13,6 +13,9 @@ public enum GlobalEqpType DoNotHideRingL, DoNotHideHrothgarHats, DoNotHideVieraHats, + HideHorns, + HideVieraEars, + HideMiqoteEars, } public static class GlobalEqpExtensions @@ -27,6 +30,9 @@ public static class GlobalEqpExtensions GlobalEqpType.DoNotHideRingL => true, GlobalEqpType.DoNotHideHrothgarHats => false, GlobalEqpType.DoNotHideVieraHats => false, + GlobalEqpType.HideHorns => false, + GlobalEqpType.HideVieraEars => false, + GlobalEqpType.HideMiqoteEars => false, _ => false, }; @@ -41,6 +47,9 @@ public static class GlobalEqpExtensions GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8, GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8, GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8, + GlobalEqpType.HideHorns => "Always Hide Horns (Au Ra)"u8, + GlobalEqpType.HideVieraEars => "Always Hide Ears (Viera)"u8, + GlobalEqpType.HideMiqoteEars => "Always Hide Ears (Miqo'te)"u8, _ => "\0"u8, }; @@ -60,6 +69,9 @@ public static class GlobalEqpExtensions "Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8, GlobalEqpType.DoNotHideVieraHats => "Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8, - _ => "\0"u8, + GlobalEqpType.HideHorns => "Forces the game to hide Au Ra horns regardless of headwear."u8, + GlobalEqpType.HideVieraEars => "Forces the game to hide Viera ears regardless of headwear."u8, + GlobalEqpType.HideMiqoteEars => "Forces the game to hide Miqo'te ears regardless of headwear."u8, + _ => "\0"u8, }; } diff --git a/schemas/structs/meta_geqp.json b/schemas/structs/meta_geqp.json index 3d4908f9..e38fbb86 100644 --- a/schemas/structs/meta_geqp.json +++ b/schemas/structs/meta_geqp.json @@ -6,7 +6,7 @@ "$ref": "meta_enums.json#U16" }, "Type": { - "enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + "enum": [ "DoNotHideEarrings", "DoNotHideNecklace", "DoNotHideBracelets", "DoNotHideRingR", "DoNotHideRingL", "DoNotHideHrothgarHats", "DoNotHideVieraHats", "HideHorns", "HideVieraEars", "HideMiqoteEars" ] } }, "required": [ "Type" ], @@ -14,7 +14,7 @@ { "properties": { "Type": { - "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats", "HideHorns", "HideVieraEars", "HideMiqoteEars" ] }, "Condition": { "const": 0 @@ -24,7 +24,7 @@ { "properties": { "Type": { - "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats" ] + "const": [ "DoNotHideHrothgarHats", "DoNotHideVieraHats", "HideHorns", "HideVieraEars", "HideMiqoteEars" ] } } }, From 3412786282f1aa52fab12c1328faa3bcfec51408 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 May 2025 12:03:04 +0200 Subject: [PATCH 702/865] Optimize used memory by metadictionarys a bit. --- Penumbra/Meta/Manipulations/MetaDictionary.cs | 445 +++++++++++------- 1 file changed, 263 insertions(+), 182 deletions(-) diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index a7225067..51ca09ab 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -11,129 +11,164 @@ namespace Penumbra.Meta.Manipulations; [JsonConverter(typeof(Converter))] public class MetaDictionary { - private readonly Dictionary _imc = []; - private readonly Dictionary _eqp = []; - private readonly Dictionary _eqdp = []; - private readonly Dictionary _est = []; - private readonly Dictionary _rsp = []; - private readonly Dictionary _gmp = []; - private readonly Dictionary _atch = []; - private readonly Dictionary _shp = []; - private readonly HashSet _globalEqp = []; + private class Wrapper : HashSet + { + public readonly Dictionary Imc = []; + public readonly Dictionary Eqp = []; + public readonly Dictionary Eqdp = []; + public readonly Dictionary Est = []; + public readonly Dictionary Rsp = []; + public readonly Dictionary Gmp = []; + public readonly Dictionary Atch = []; + public readonly Dictionary Shp = []; + + public Wrapper() + { } + + public Wrapper(MetaCache cache) + { + Imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + Eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); + Est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + foreach (var geqp in cache.GlobalEqp.Keys) + Add(geqp); + } + } + + private Wrapper? _data; public IReadOnlyDictionary Imc - => _imc; + => _data?.Imc ?? []; public IReadOnlyDictionary Eqp - => _eqp; + => _data?.Eqp ?? []; public IReadOnlyDictionary Eqdp - => _eqdp; + => _data?.Eqdp ?? []; public IReadOnlyDictionary Est - => _est; + => _data?.Est ?? []; public IReadOnlyDictionary Gmp - => _gmp; + => _data?.Gmp ?? []; public IReadOnlyDictionary Rsp - => _rsp; + => _data?.Rsp ?? []; public IReadOnlyDictionary Atch - => _atch; + => _data?.Atch ?? []; public IReadOnlyDictionary Shp - => _shp; + => _data?.Shp ?? []; public IReadOnlySet GlobalEqp - => _globalEqp; + => _data ?? []; public int Count { get; private set; } public int GetCount(MetaManipulationType type) - => type switch - { - MetaManipulationType.Imc => _imc.Count, - MetaManipulationType.Eqdp => _eqdp.Count, - MetaManipulationType.Eqp => _eqp.Count, - MetaManipulationType.Est => _est.Count, - MetaManipulationType.Gmp => _gmp.Count, - MetaManipulationType.Rsp => _rsp.Count, - MetaManipulationType.Atch => _atch.Count, - MetaManipulationType.Shp => _shp.Count, - MetaManipulationType.GlobalEqp => _globalEqp.Count, - _ => 0, - }; + => _data is null + ? 0 + : type switch + { + MetaManipulationType.Imc => _data.Imc.Count, + MetaManipulationType.Eqdp => _data.Eqdp.Count, + MetaManipulationType.Eqp => _data.Eqp.Count, + MetaManipulationType.Est => _data.Est.Count, + MetaManipulationType.Gmp => _data.Gmp.Count, + MetaManipulationType.Rsp => _data.Rsp.Count, + MetaManipulationType.Atch => _data.Atch.Count, + MetaManipulationType.Shp => _data.Shp.Count, + MetaManipulationType.GlobalEqp => _data.Count, + _ => 0, + }; public bool Contains(IMetaIdentifier identifier) - => identifier switch - { - EqdpIdentifier i => _eqdp.ContainsKey(i), - EqpIdentifier i => _eqp.ContainsKey(i), - EstIdentifier i => _est.ContainsKey(i), - GlobalEqpManipulation i => _globalEqp.Contains(i), - GmpIdentifier i => _gmp.ContainsKey(i), - ImcIdentifier i => _imc.ContainsKey(i), - AtchIdentifier i => _atch.ContainsKey(i), - ShpIdentifier i => _shp.ContainsKey(i), - RspIdentifier i => _rsp.ContainsKey(i), - _ => false, - }; + => _data is not null + && identifier switch + { + EqdpIdentifier i => _data.Eqdp.ContainsKey(i), + EqpIdentifier i => _data.Eqp.ContainsKey(i), + EstIdentifier i => _data.Est.ContainsKey(i), + GlobalEqpManipulation i => _data.Contains(i), + GmpIdentifier i => _data.Gmp.ContainsKey(i), + ImcIdentifier i => _data.Imc.ContainsKey(i), + AtchIdentifier i => _data.Atch.ContainsKey(i), + ShpIdentifier i => _data.Shp.ContainsKey(i), + RspIdentifier i => _data.Rsp.ContainsKey(i), + _ => false, + }; public void Clear() { + _data = null; Count = 0; - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _est.Clear(); - _rsp.Clear(); - _gmp.Clear(); - _atch.Clear(); - _shp.Clear(); - _globalEqp.Clear(); } public void ClearForDefault() { - Count = _globalEqp.Count; - _imc.Clear(); - _eqp.Clear(); - _eqdp.Clear(); - _est.Clear(); - _rsp.Clear(); - _gmp.Clear(); - _atch.Clear(); + if (_data is null) + return; + + if (_data.Count is 0 && Shp.Count is 0) + { + _data = null; + Count = 0; + } + + Count = GlobalEqp.Count + Shp.Count; + _data!.Imc.Clear(); + _data!.Eqp.Clear(); + _data!.Eqdp.Clear(); + _data!.Est.Clear(); + _data!.Rsp.Clear(); + _data!.Gmp.Clear(); + _data!.Atch.Clear(); } public bool Equals(MetaDictionary other) - => Count == other.Count - && _imc.SetEquals(other._imc) - && _eqp.SetEquals(other._eqp) - && _eqdp.SetEquals(other._eqdp) - && _est.SetEquals(other._est) - && _rsp.SetEquals(other._rsp) - && _gmp.SetEquals(other._gmp) - && _atch.SetEquals(other._atch) - && _shp.SetEquals(other._shp) - && _globalEqp.SetEquals(other._globalEqp); + { + if (Count != other.Count) + return false; + + if (_data is null) + return true; + + return _data.Imc.SetEquals(other._data!.Imc) + && _data.Eqp.SetEquals(other._data!.Eqp) + && _data.Eqdp.SetEquals(other._data!.Eqdp) + && _data.Est.SetEquals(other._data!.Est) + && _data.Rsp.SetEquals(other._data!.Rsp) + && _data.Gmp.SetEquals(other._data!.Gmp) + && _data.Atch.SetEquals(other._data!.Atch) + && _data.Shp.SetEquals(other._data!.Shp) + && _data.SetEquals(other._data!); + } public IEnumerable Identifiers - => _imc.Keys.Cast() - .Concat(_eqdp.Keys.Cast()) - .Concat(_eqp.Keys.Cast()) - .Concat(_est.Keys.Cast()) - .Concat(_gmp.Keys.Cast()) - .Concat(_rsp.Keys.Cast()) - .Concat(_atch.Keys.Cast()) - .Concat(_shp.Keys.Cast()) - .Concat(_globalEqp.Cast()); + => _data is null + ? [] + : _data.Imc.Keys.Cast() + .Concat(_data!.Eqdp.Keys.Cast()) + .Concat(_data!.Eqp.Keys.Cast()) + .Concat(_data!.Est.Keys.Cast()) + .Concat(_data!.Gmp.Keys.Cast()) + .Concat(_data!.Rsp.Keys.Cast()) + .Concat(_data!.Atch.Keys.Cast()) + .Concat(_data!.Shp.Keys.Cast()) + .Concat(_data!.Cast()); #region TryAdd public bool TryAdd(ImcIdentifier identifier, ImcEntry entry) { - if (!_imc.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Imc.TryAdd(identifier, entry)) return false; ++Count; @@ -142,7 +177,8 @@ public class MetaDictionary public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry) { - if (!_eqp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Eqp.TryAdd(identifier, entry)) return false; ++Count; @@ -154,7 +190,8 @@ public class MetaDictionary public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry) { - if (!_eqdp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Eqdp.TryAdd(identifier, entry)) return false; ++Count; @@ -166,7 +203,8 @@ public class MetaDictionary public bool TryAdd(EstIdentifier identifier, EstEntry entry) { - if (!_est.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Est.TryAdd(identifier, entry)) return false; ++Count; @@ -175,7 +213,8 @@ public class MetaDictionary public bool TryAdd(GmpIdentifier identifier, GmpEntry entry) { - if (!_gmp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Gmp.TryAdd(identifier, entry)) return false; ++Count; @@ -184,7 +223,8 @@ public class MetaDictionary public bool TryAdd(RspIdentifier identifier, RspEntry entry) { - if (!_rsp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Rsp.TryAdd(identifier, entry)) return false; ++Count; @@ -193,7 +233,8 @@ public class MetaDictionary public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry) { - if (!_atch.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Atch.TryAdd(identifier, entry)) return false; ++Count; @@ -202,7 +243,8 @@ public class MetaDictionary public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry) { - if (!_shp.TryAdd(identifier, entry)) + _data ??= []; + if (!_data!.Shp.TryAdd(identifier, entry)) return false; ++Count; @@ -211,7 +253,8 @@ public class MetaDictionary public bool TryAdd(GlobalEqpManipulation identifier) { - if (!_globalEqp.Add(identifier)) + _data ??= []; + if (!_data.Add(identifier)) return false; ++Count; @@ -224,19 +267,19 @@ public class MetaDictionary public bool Update(ImcIdentifier identifier, ImcEntry entry) { - if (!_imc.ContainsKey(identifier)) + if (_data is null || !_data.Imc.ContainsKey(identifier)) return false; - _imc[identifier] = entry; + _data.Imc[identifier] = entry; return true; } public bool Update(EqpIdentifier identifier, EqpEntryInternal entry) { - if (!_eqp.ContainsKey(identifier)) + if (_data is null || !_data.Eqp.ContainsKey(identifier)) return false; - _eqp[identifier] = entry; + _data.Eqp[identifier] = entry; return true; } @@ -245,10 +288,10 @@ public class MetaDictionary public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry) { - if (!_eqdp.ContainsKey(identifier)) + if (_data is null || !_data.Eqdp.ContainsKey(identifier)) return false; - _eqdp[identifier] = entry; + _data.Eqdp[identifier] = entry; return true; } @@ -257,46 +300,46 @@ public class MetaDictionary public bool Update(EstIdentifier identifier, EstEntry entry) { - if (!_est.ContainsKey(identifier)) + if (_data is null || !_data.Est.ContainsKey(identifier)) return false; - _est[identifier] = entry; + _data.Est[identifier] = entry; return true; } public bool Update(GmpIdentifier identifier, GmpEntry entry) { - if (!_gmp.ContainsKey(identifier)) + if (_data is null || !_data.Gmp.ContainsKey(identifier)) return false; - _gmp[identifier] = entry; + _data.Gmp[identifier] = entry; return true; } public bool Update(RspIdentifier identifier, RspEntry entry) { - if (!_rsp.ContainsKey(identifier)) + if (_data is null || !_data.Rsp.ContainsKey(identifier)) return false; - _rsp[identifier] = entry; + _data.Rsp[identifier] = entry; return true; } public bool Update(AtchIdentifier identifier, in AtchEntry entry) { - if (!_atch.ContainsKey(identifier)) + if (_data is null || !_data.Atch.ContainsKey(identifier)) return false; - _atch[identifier] = entry; + _data.Atch[identifier] = entry; return true; } public bool Update(ShpIdentifier identifier, in ShpEntry entry) { - if (!_shp.ContainsKey(identifier)) + if (_data is null || !_data.Shp.ContainsKey(identifier)) return false; - _shp[identifier] = entry; + _data.Shp[identifier] = entry; return true; } @@ -305,48 +348,59 @@ public class MetaDictionary #region TryGetValue public bool TryGetValue(EstIdentifier identifier, out EstEntry value) - => _est.TryGetValue(identifier, out value); + => _data?.Est.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value) - => _eqp.TryGetValue(identifier, out value); + => _data?.Eqp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value) - => _eqdp.TryGetValue(identifier, out value); + => _data?.Eqdp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value) - => _gmp.TryGetValue(identifier, out value); + => _data?.Gmp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(RspIdentifier identifier, out RspEntry value) - => _rsp.TryGetValue(identifier, out value); + => _data?.Rsp.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value) - => _imc.TryGetValue(identifier, out value); + => _data?.Imc.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value) - => _atch.TryGetValue(identifier, out value); + => _data?.Atch.TryGetValue(identifier, out value) ?? SetDefault(out value); public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) - => _shp.TryGetValue(identifier, out value); + => _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool SetDefault(out T? value) + { + value = default; + return false; + } #endregion public bool Remove(IMetaIdentifier identifier) { + if (_data is null) + return false; + var ret = identifier switch { - EqdpIdentifier i => _eqdp.Remove(i), - EqpIdentifier i => _eqp.Remove(i), - EstIdentifier i => _est.Remove(i), - GlobalEqpManipulation i => _globalEqp.Remove(i), - GmpIdentifier i => _gmp.Remove(i), - ImcIdentifier i => _imc.Remove(i), - RspIdentifier i => _rsp.Remove(i), - AtchIdentifier i => _atch.Remove(i), - ShpIdentifier i => _shp.Remove(i), + EqdpIdentifier i => _data.Eqdp.Remove(i), + EqpIdentifier i => _data.Eqp.Remove(i), + EstIdentifier i => _data.Est.Remove(i), + GlobalEqpManipulation i => _data.Remove(i), + GmpIdentifier i => _data.Gmp.Remove(i), + ImcIdentifier i => _data.Imc.Remove(i), + RspIdentifier i => _data.Rsp.Remove(i), + AtchIdentifier i => _data.Atch.Remove(i), + ShpIdentifier i => _data.Shp.Remove(i), _ => false, }; - if (ret) - --Count; + if (ret && --Count is 0) + _data = null; + return ret; } @@ -354,86 +408,97 @@ public class MetaDictionary public void UnionWith(MetaDictionary manips) { - foreach (var (identifier, entry) in manips._imc) + if (manips.Count is 0) + return; + + _data ??= []; + foreach (var (identifier, entry) in manips._data!.Imc) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._eqp) + foreach (var (identifier, entry) in manips._data!.Eqp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._eqdp) + foreach (var (identifier, entry) in manips._data!.Eqdp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._gmp) + foreach (var (identifier, entry) in manips._data!.Gmp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._rsp) + foreach (var (identifier, entry) in manips._data!.Rsp) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._est) + foreach (var (identifier, entry) in manips._data!.Est) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._atch) + foreach (var (identifier, entry) in manips._data!.Atch) TryAdd(identifier, entry); - foreach (var (identifier, entry) in manips._shp) + foreach (var (identifier, entry) in manips._data!.Shp) TryAdd(identifier, entry); - foreach (var identifier in manips._globalEqp) + foreach (var identifier in manips._data!) TryAdd(identifier); } /// Try to merge all manipulations from manips into this, and return the first failure, if any. public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier) { - foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + if (manips.Count is 0) + { + failedIdentifier = null; + return true; + } + + _data ??= []; + foreach (var (identifier, _) in manips._data!.Imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Est.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + foreach (var (identifier, _) in manips._data!.Shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) { failedIdentifier = identifier; return false; } - foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier))) + foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; return false; @@ -445,30 +510,50 @@ public class MetaDictionary public void SetTo(MetaDictionary other) { - _imc.SetTo(other._imc); - _eqp.SetTo(other._eqp); - _eqdp.SetTo(other._eqdp); - _est.SetTo(other._est); - _rsp.SetTo(other._rsp); - _gmp.SetTo(other._gmp); - _atch.SetTo(other._atch); - _shp.SetTo(other._shp); - _globalEqp.SetTo(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; + if (other.Count is 0) + { + _data = null; + Count = 0; + return; + } + + _data ??= []; + _data!.Imc.SetTo(other._data!.Imc); + _data!.Eqp.SetTo(other._data!.Eqp); + _data!.Eqdp.SetTo(other._data!.Eqdp); + _data!.Est.SetTo(other._data!.Est); + _data!.Rsp.SetTo(other._data!.Rsp); + _data!.Gmp.SetTo(other._data!.Gmp); + _data!.Atch.SetTo(other._data!.Atch); + _data!.Shp.SetTo(other._data!.Shp); + _data!.SetTo(other._data!); + Count = other.Count; } public void UpdateTo(MetaDictionary other) { - _imc.UpdateTo(other._imc); - _eqp.UpdateTo(other._eqp); - _eqdp.UpdateTo(other._eqdp); - _est.UpdateTo(other._est); - _rsp.UpdateTo(other._rsp); - _gmp.UpdateTo(other._gmp); - _atch.UpdateTo(other._atch); - _shp.UpdateTo(other._shp); - _globalEqp.UnionWith(other._globalEqp); - Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count; + if (other.Count is 0) + return; + + _data ??= []; + _data!.Imc.UpdateTo(other._data!.Imc); + _data!.Eqp.UpdateTo(other._data!.Eqp); + _data!.Eqdp.UpdateTo(other._data!.Eqdp); + _data!.Est.UpdateTo(other._data!.Est); + _data!.Rsp.UpdateTo(other._data!.Rsp); + _data!.Gmp.UpdateTo(other._data!.Gmp); + _data!.Atch.UpdateTo(other._data!.Atch); + _data!.Shp.UpdateTo(other._data!.Shp); + _data!.UnionWith(other._data!); + Count = _data!.Imc.Count + + _data!.Eqp.Count + + _data!.Eqdp.Count + + _data!.Est.Count + + _data!.Rsp.Count + + _data!.Gmp.Count + + _data!.Atch.Count + + _data!.Shp.Count + + _data!.Count; } #endregion @@ -635,15 +720,19 @@ public class MetaDictionary } var array = new JArray(); - SerializeTo(array, value._imc); - SerializeTo(array, value._eqp); - SerializeTo(array, value._eqdp); - SerializeTo(array, value._est); - SerializeTo(array, value._rsp); - SerializeTo(array, value._gmp); - SerializeTo(array, value._atch); - SerializeTo(array, value._shp); - SerializeTo(array, value._globalEqp); + if (value._data is not null) + { + SerializeTo(array, value._data!.Imc); + SerializeTo(array, value._data!.Eqp); + SerializeTo(array, value._data!.Eqdp); + SerializeTo(array, value._data!.Est); + SerializeTo(array, value._data!.Rsp); + SerializeTo(array, value._data!.Gmp); + SerializeTo(array, value._data!.Atch); + SerializeTo(array, value._data!.Shp); + SerializeTo(array, value._data!); + } + array.WriteTo(writer); } @@ -771,18 +860,10 @@ public class MetaDictionary public MetaDictionary(MetaCache? cache) { - if (cache == null) + if (cache is null) return; - _imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); - _eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot)); - _est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); - _globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet(); - Count = cache.Count; + _data = new Wrapper(cache); + Count = cache.Count; } } From d7dee39fab37fcedf047b4fb2829f034526bf63d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 May 2025 15:44:26 +0200 Subject: [PATCH 703/865] Add attribute handling, rework atr and shape caches. --- Penumbra.GameData | 2 +- Penumbra.sln | 1 + Penumbra/Collections/Cache/AtrCache.cs | 56 ++++ Penumbra/Collections/Cache/CollectionCache.cs | 2 + Penumbra/Collections/Cache/MetaCache.cs | 9 +- .../Cache/ShapeAttributeHashSet.cs | 123 ++++++++ Penumbra/Collections/Cache/ShpCache.cs | 85 +----- .../Hooks/PostProcessing/AttributeHook.cs | 4 +- Penumbra/Meta/Manipulations/AtrIdentifier.cs | 145 +++++++++ .../Meta/Manipulations/IMetaIdentifier.cs | 1 + Penumbra/Meta/Manipulations/MetaDictionary.cs | 70 ++++- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 73 ++--- Penumbra/Meta/ShapeAttributeManager.cs | 153 ++++++++++ ...ShapeString.cs => ShapeAttributeString.cs} | 95 +++++- Penumbra/Meta/ShapeManager.cs | 140 --------- .../UI/AdvancedWindow/Meta/AtrMetaDrawer.cs | 274 ++++++++++++++++++ .../UI/AdvancedWindow/Meta/MetaDrawers.cs | 5 +- .../UI/AdvancedWindow/Meta/ShpMetaDrawer.cs | 74 ++++- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 1 + Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 132 ++++++++- schemas/structs/manipulation.json | 12 +- schemas/structs/meta_atr.json | 27 ++ schemas/structs/meta_shp.json | 3 + 23 files changed, 1187 insertions(+), 300 deletions(-) create mode 100644 Penumbra/Collections/Cache/AtrCache.cs create mode 100644 Penumbra/Collections/Cache/ShapeAttributeHashSet.cs create mode 100644 Penumbra/Meta/Manipulations/AtrIdentifier.cs create mode 100644 Penumbra/Meta/ShapeAttributeManager.cs rename Penumbra/Meta/{ShapeString.cs => ShapeAttributeString.cs} (55%) delete mode 100644 Penumbra/Meta/ShapeManager.cs create mode 100644 Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs create mode 100644 schemas/structs/meta_atr.json diff --git a/Penumbra.GameData b/Penumbra.GameData index b15c0f07..bb3b462b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit b15c0f07ba270a7b6a350411006e003da9818d1b +Subproject commit bb3b462bbc5bc2a598c1ad8c372b0cb255551fe1 diff --git a/Penumbra.sln b/Penumbra.sln index 642876ef..fbcd6080 100644 --- a/Penumbra.sln +++ b/Penumbra.sln @@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F schemas\structs\group_single.json = schemas\structs\group_single.json schemas\structs\manipulation.json = schemas\structs\manipulation.json schemas\structs\meta_atch.json = schemas\structs\meta_atch.json + schemas\structs\meta_atr.json = schemas\structs\meta_atr.json schemas\structs\meta_enums.json = schemas\structs\meta_enums.json schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json diff --git a/Penumbra/Collections/Cache/AtrCache.cs b/Penumbra/Collections/Cache/AtrCache.cs new file mode 100644 index 00000000..757ddaa2 --- /dev/null +++ b/Penumbra/Collections/Cache/AtrCache.cs @@ -0,0 +1,56 @@ +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Collections.Cache; + +public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) +{ + public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.Contains(slot, id, genderRace); + + public int DisabledCount { get; private set; } + + internal IReadOnlyDictionary Data + => _atrData; + + private readonly Dictionary _atrData = []; + + public void Reset() + { + Clear(); + _atrData.Clear(); + } + + protected override void Dispose(bool _) + => Clear(); + + protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry) + { + if (!_atrData.TryGetValue(identifier.Attribute, out var value)) + { + if (entry.Value) + return; + + value = []; + _atrData.Add(identifier.Attribute, value); + } + + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, !entry.Value)) + ++DisabledCount; + } + + protected override void RevertModInternal(AtrIdentifier identifier) + { + if (!_atrData.TryGetValue(identifier.Attribute, out var value)) + return; + + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + { + --DisabledCount; + if (value.IsEmpty) + _atrData.Remove(identifier.Attribute); + } + } +} diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index c48a487c..8294624b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -247,6 +247,8 @@ public sealed class CollectionCache : IDisposable AddManipulation(mod, identifier, entry); foreach (var (identifier, entry) in files.Manipulations.Shp) AddManipulation(mod, identifier, entry); + foreach (var (identifier, entry) in files.Manipulations.Atr) + AddManipulation(mod, identifier, entry); foreach (var identifier in files.Manipulations.GlobalEqp) AddManipulation(mod, identifier, null!); } diff --git a/Penumbra/Collections/Cache/MetaCache.cs b/Penumbra/Collections/Cache/MetaCache.cs index 790dd3af..011cdd23 100644 --- a/Penumbra/Collections/Cache/MetaCache.cs +++ b/Penumbra/Collections/Cache/MetaCache.cs @@ -17,11 +17,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) public readonly ImcCache Imc = new(manager, collection); public readonly AtchCache Atch = new(manager, collection); public readonly ShpCache Shp = new(manager, collection); + public readonly AtrCache Atr = new(manager, collection); public readonly GlobalEqpCache GlobalEqp = new(); public bool IsDisposed { get; private set; } public int Count - => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + GlobalEqp.Count; + => Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count; public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources => Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)) @@ -32,6 +33,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) .Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) + .Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))) .Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value))); public void Reset() @@ -44,6 +46,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Imc.Reset(); Atch.Reset(); Shp.Reset(); + Atr.Reset(); GlobalEqp.Clear(); } @@ -61,6 +64,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) Imc.Dispose(); Atch.Dispose(); Shp.Dispose(); + Atr.Dispose(); } public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod) @@ -76,6 +80,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod), AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod), ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod), + AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod), GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod), _ => false, }; @@ -98,6 +103,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i => Rsp.RevertMod(i, out mod), AtchIdentifier i => Atch.RevertMod(i, out mod), ShpIdentifier i => Shp.RevertMod(i, out mod), + AtrIdentifier i => Atr.RevertMod(i, out mod), GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod), _ => (mod = null) != null, }; @@ -115,6 +121,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection) RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e), AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e), ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e), + AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e), GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i), _ => false, }; diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs new file mode 100644 index 00000000..f1fc7127 --- /dev/null +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -0,0 +1,123 @@ +using System.Collections.Frozen; +using OtterGui.Extensions; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; + +namespace Penumbra.Collections.Cache; + +public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryId Id), ulong> +{ + public static readonly IReadOnlyList GenderRaceValues = + [ + GenderRace.Unknown, GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale, + GenderRace.ElezenMale, GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale, + GenderRace.RoegadynFemale, GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale, + GenderRace.HrothgarMale, GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale, + ]; + + public static readonly FrozenDictionary GenderRaceIndices = + GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index); + + private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + + public bool this[HumanSlot slot] + => slot is HumanSlot.Unknown ? All : _allIds[(int)slot * GenderRaceIndices.Count]; + + public bool this[GenderRace genderRace] + => GenderRaceIndices.TryGetValue(genderRace, out var index) + && _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]; + + public bool this[HumanSlot slot, GenderRace genderRace] + { + get + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; + + if (_allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]) + return true; + + return _allIds[(int)slot * GenderRaceIndices.Count + index]; + } + set + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return; + + var genderRaceCount = GenderRaceValues.Count; + if (slot is HumanSlot.Unknown) + _allIds[ShapeAttributeManager.ModelSlotSize * genderRaceCount + index] = value; + else + _allIds[(int)slot * genderRaceCount + index] = value; + } + } + + public bool All + => _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count]; + + public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) + => All || this[slot, genderRace] || ContainsEntry(slot, id, genderRace); + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) + => GenderRaceIndices.TryGetValue(genderRace, out var index) + && TryGetValue((slot, id), out var flags) + && (flags & (1ul << index)) is not 0; + + public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) + { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; + + if (!id.HasValue) + { + var slotIndex = slot is HumanSlot.Unknown ? ShapeAttributeManager.ModelSlotSize : (int)slot; + var old = _allIds[slotIndex * GenderRaceIndices.Count + index]; + _allIds[slotIndex * GenderRaceIndices.Count + index] = value; + return old != value; + } + + if (value) + { + if (TryGetValue((slot, id.Value), out var flags)) + { + var newFlags = flags | (1ul << index); + if (newFlags == flags) + return false; + + this[(slot, id.Value)] = newFlags; + return true; + } + + this[(slot, id.Value)] = 1ul << index; + return true; + } + else if (TryGetValue((slot, id.Value), out var flags)) + { + var newFlags = flags & ~(1ul << index); + if (newFlags == flags) + return false; + + if (newFlags is 0) + { + Remove((slot, id.Value)); + return true; + } + + this[(slot, id.Value)] = newFlags; + return true; + } + + return false; + } + + public new void Clear() + { + base.Clear(); + _allIds.SetAll(false); + } + + public bool IsEmpty + => !_allIds.HasAnySet() && Count is 0; +} diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 2fe7f933..22547d25 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -7,10 +7,10 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { - public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id) - => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id); + public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id, genderRace); - internal IReadOnlyDictionary State(ShapeConnectorCondition connector) + internal IReadOnlyDictionary State(ShapeConnectorCondition connector) => connector switch { ShapeConnectorCondition.None => _shpData, @@ -22,73 +22,10 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) public int EnabledCount { get; private set; } - public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)> - { - private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize); - - public bool All - { - get => _allIds[^1]; - set => _allIds[^1] = value; - } - - public bool this[HumanSlot slot] - { - get - { - if (slot is HumanSlot.Unknown) - return All; - - return _allIds[(int)slot]; - } - set - { - if (slot is HumanSlot.Unknown) - _allIds[^1] = value; - else - _allIds[(int)slot] = value; - } - } - - public bool Contains(HumanSlot slot, PrimaryId id) - => All || this[slot] || Contains((slot, id)); - - public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value) - { - if (slot is HumanSlot.Unknown) - { - var old = All; - All = value.Value; - return old != value.Value; - } - - if (!id.HasValue) - { - var old = this[slot]; - this[slot] = value.Value; - return old != value.Value; - } - - if (value.Value) - return Add((slot, id.Value)); - - return Remove((slot, id.Value)); - } - - public new void Clear() - { - base.Clear(); - _allIds.SetAll(false); - } - - public bool IsEmpty - => !_allIds.HasAnySet() && Count is 0; - } - - private readonly Dictionary _shpData = []; - private readonly Dictionary _wristConnectors = []; - private readonly Dictionary _waistConnectors = []; - private readonly Dictionary _ankleConnectors = []; + private readonly Dictionary _shpData = []; + private readonly Dictionary _wristConnectors = []; + private readonly Dictionary _waistConnectors = []; + private readonly Dictionary _ankleConnectors = []; public void Reset() { @@ -114,7 +51,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; - void Func(Dictionary dict) + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) { @@ -125,7 +62,7 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) dict.Add(identifier.Shape, value); } - if (value.TrySet(identifier.Slot, identifier.Id, entry)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value)) ++EnabledCount; } } @@ -142,12 +79,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) return; - void Func(Dictionary dict) + void Func(Dictionary dict) { if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) { --EnabledCount; if (value.IsEmpty) diff --git a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs index cad049ad..00e5851f 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs @@ -22,8 +22,8 @@ public sealed unsafe class AttributeHook : EventWrapper - ShapeManager = 0, + /// + ShapeAttributeManager = 0, } private readonly CollectionResolver _resolver; diff --git a/Penumbra/Meta/Manipulations/AtrIdentifier.cs b/Penumbra/Meta/Manipulations/AtrIdentifier.cs new file mode 100644 index 00000000..ca65f6aa --- /dev/null +++ b/Penumbra/Meta/Manipulations/AtrIdentifier.cs @@ -0,0 +1,145 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Structs; +using Penumbra.Meta.Files; + +namespace Penumbra.Meta.Manipulations; + +public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeAttributeString Attribute, GenderRace GenderRaceCondition) + : IComparable, IMetaIdentifier +{ + public int CompareTo(AtrIdentifier other) + { + var slotComparison = Slot.CompareTo(other.Slot); + if (slotComparison is not 0) + return slotComparison; + + if (Id.HasValue) + { + if (other.Id.HasValue) + { + var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id); + if (idComparison is not 0) + return idComparison; + } + else + { + return -1; + } + } + else if (other.Id.HasValue) + { + return 1; + } + + var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition); + if (genderRaceComparison is not 0) + return genderRaceComparison; + + return Attribute.CompareTo(other.Attribute); + } + + + public override string ToString() + { + var sb = new StringBuilder(64); + sb.Append("Shp - ") + .Append(Attribute); + if (Slot is HumanSlot.Unknown) + { + sb.Append(" - All Slots & IDs"); + } + else + { + sb.Append(" - ") + .Append(Slot.ToName()) + .Append(" - "); + if (Id.HasValue) + sb.Append(Id.Value.Id); + else + sb.Append("All IDs"); + } + + if (GenderRaceCondition is not GenderRace.Unknown) + sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode()); + + return sb.ToString(); + } + + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + { + // Nothing for now since it depends entirely on the shape key. + } + + public MetaIndex FileIndex() + => (MetaIndex)(-1); + + public bool Validate() + { + if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) + return false; + + if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition)) + return false; + + if (Slot is HumanSlot.Unknown && Id is not null) + return false; + + if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue }) + return false; + + if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) + return false; + + return Attribute.ValidateCustomAttributeString(); + } + + public JObject AddToJson(JObject jObj) + { + if (Slot is not HumanSlot.Unknown) + jObj["Slot"] = Slot.ToString(); + if (Id.HasValue) + jObj["Id"] = Id.Value.Id.ToString(); + jObj["Attribute"] = Attribute.ToString(); + if (GenderRaceCondition is not GenderRace.Unknown) + jObj["GenderRaceCondition"] = (uint)GenderRaceCondition; + return jObj; + } + + public static AtrIdentifier? FromJson(JObject jObj) + { + var attribute = jObj["Attribute"]?.ToObject(); + if (attribute is null || !ShapeAttributeString.TryRead(attribute, out var attributeString)) + return null; + + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject() ?? 0; + var identifier = new AtrIdentifier(slot, id, attributeString, genderRaceCondition); + return identifier.Validate() ? identifier : null; + } + + public MetaManipulationType Type + => MetaManipulationType.Atr; +} + +[JsonConverter(typeof(Converter))] +public readonly record struct AtrEntry(bool Value) +{ + public static readonly AtrEntry True = new(true); + public static readonly AtrEntry False = new(false); + + private class Converter : JsonConverter + { + public override void WriteJson(JsonWriter writer, AtrEntry value, JsonSerializer serializer) + => serializer.Serialize(writer, value.Value); + + public override AtrEntry ReadJson(JsonReader reader, Type objectType, AtrEntry existingValue, bool hasExistingValue, + JsonSerializer serializer) + => new(serializer.Deserialize(reader)); + } +} diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 13feba51..922825c3 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -16,6 +16,7 @@ public enum MetaManipulationType : byte GlobalEqp = 7, Atch = 8, Shp = 9, + Atr = 10, } public interface IMetaIdentifier diff --git a/Penumbra/Meta/Manipulations/MetaDictionary.cs b/Penumbra/Meta/Manipulations/MetaDictionary.cs index 51ca09ab..23eaec76 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -21,6 +21,7 @@ public class MetaDictionary public readonly Dictionary Gmp = []; public readonly Dictionary Atch = []; public readonly Dictionary Shp = []; + public readonly Dictionary Atr = []; public Wrapper() { } @@ -35,6 +36,7 @@ public class MetaDictionary Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); + Atr = cache.Atr.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry); foreach (var geqp in cache.GlobalEqp.Keys) Add(geqp); } @@ -66,6 +68,9 @@ public class MetaDictionary public IReadOnlyDictionary Shp => _data?.Shp ?? []; + public IReadOnlyDictionary Atr + => _data?.Atr ?? []; + public IReadOnlySet GlobalEqp => _data ?? []; @@ -84,6 +89,7 @@ public class MetaDictionary MetaManipulationType.Rsp => _data.Rsp.Count, MetaManipulationType.Atch => _data.Atch.Count, MetaManipulationType.Shp => _data.Shp.Count, + MetaManipulationType.Atr => _data.Atr.Count, MetaManipulationType.GlobalEqp => _data.Count, _ => 0, }; @@ -100,6 +106,7 @@ public class MetaDictionary ImcIdentifier i => _data.Imc.ContainsKey(i), AtchIdentifier i => _data.Atch.ContainsKey(i), ShpIdentifier i => _data.Shp.ContainsKey(i), + AtrIdentifier i => _data.Atr.ContainsKey(i), RspIdentifier i => _data.Rsp.ContainsKey(i), _ => false, }; @@ -115,13 +122,13 @@ public class MetaDictionary if (_data is null) return; - if (_data.Count is 0 && Shp.Count is 0) + if (_data.Count is 0 && Shp.Count is 0 && Atr.Count is 0) { _data = null; Count = 0; } - Count = GlobalEqp.Count + Shp.Count; + Count = GlobalEqp.Count + Shp.Count + Atr.Count; _data!.Imc.Clear(); _data!.Eqp.Clear(); _data!.Eqdp.Clear(); @@ -147,6 +154,7 @@ public class MetaDictionary && _data.Gmp.SetEquals(other._data!.Gmp) && _data.Atch.SetEquals(other._data!.Atch) && _data.Shp.SetEquals(other._data!.Shp) + && _data.Atr.SetEquals(other._data!.Atr) && _data.SetEquals(other._data!); } @@ -161,6 +169,7 @@ public class MetaDictionary .Concat(_data!.Rsp.Keys.Cast()) .Concat(_data!.Atch.Keys.Cast()) .Concat(_data!.Shp.Keys.Cast()) + .Concat(_data!.Atr.Keys.Cast()) .Concat(_data!.Cast()); #region TryAdd @@ -251,6 +260,16 @@ public class MetaDictionary return true; } + public bool TryAdd(AtrIdentifier identifier, in AtrEntry entry) + { + _data ??= []; + if (!_data!.Atr.TryAdd(identifier, entry)) + return false; + + ++Count; + return true; + } + public bool TryAdd(GlobalEqpManipulation identifier) { _data ??= []; @@ -343,6 +362,15 @@ public class MetaDictionary return true; } + public bool Update(AtrIdentifier identifier, in AtrEntry entry) + { + if (_data is null || !_data.Atr.ContainsKey(identifier)) + return false; + + _data.Atr[identifier] = entry; + return true; + } + #endregion #region TryGetValue @@ -371,6 +399,9 @@ public class MetaDictionary public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value) => _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value); + public bool TryGetValue(AtrIdentifier identifier, out AtrEntry value) + => _data?.Atr.TryGetValue(identifier, out value) ?? SetDefault(out value); + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static bool SetDefault(out T? value) { @@ -396,6 +427,7 @@ public class MetaDictionary RspIdentifier i => _data.Rsp.Remove(i), AtchIdentifier i => _data.Atch.Remove(i), ShpIdentifier i => _data.Shp.Remove(i), + AtrIdentifier i => _data.Atr.Remove(i), _ => false, }; if (ret && --Count is 0) @@ -436,6 +468,9 @@ public class MetaDictionary foreach (var (identifier, entry) in manips._data!.Shp) TryAdd(identifier, entry); + foreach (var (identifier, entry) in manips._data!.Atr) + TryAdd(identifier, entry); + foreach (var identifier in manips._data!) TryAdd(identifier); } @@ -498,6 +533,12 @@ public class MetaDictionary return false; } + foreach (var (identifier, _) in manips._data!.Atr.Where(kvp => !TryAdd(kvp.Key, kvp.Value))) + { + failedIdentifier = identifier; + return false; + } + foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier))) { failedIdentifier = identifier; @@ -526,6 +567,7 @@ public class MetaDictionary _data!.Gmp.SetTo(other._data!.Gmp); _data!.Atch.SetTo(other._data!.Atch); _data!.Shp.SetTo(other._data!.Shp); + _data!.Atr.SetTo(other._data!.Atr); _data!.SetTo(other._data!); Count = other.Count; } @@ -544,6 +586,7 @@ public class MetaDictionary _data!.Gmp.UpdateTo(other._data!.Gmp); _data!.Atch.UpdateTo(other._data!.Atch); _data!.Shp.UpdateTo(other._data!.Shp); + _data!.Atr.UpdateTo(other._data!.Atr); _data!.UnionWith(other._data!); Count = _data!.Imc.Count + _data!.Eqp.Count @@ -651,6 +694,16 @@ public class MetaDictionary }), }; + public static JObject Serialize(AtrIdentifier identifier, AtrEntry entry) + => new() + { + ["Type"] = MetaManipulationType.Atr.ToString(), + ["Manipulation"] = identifier.AddToJson(new JObject + { + ["Entry"] = entry.Value, + }), + }; + public static JObject Serialize(GlobalEqpManipulation identifier) => new() { @@ -682,6 +735,8 @@ public class MetaDictionary return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry)) return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); + if (typeof(TIdentifier) == typeof(AtrIdentifier) && typeof(TEntry) == typeof(AtrEntry)) + return Serialize(Unsafe.As(ref identifier), Unsafe.As(ref entry)); if (typeof(TIdentifier) == typeof(GlobalEqpManipulation)) return Serialize(Unsafe.As(ref identifier)); @@ -730,6 +785,7 @@ public class MetaDictionary SerializeTo(array, value._data!.Gmp); SerializeTo(array, value._data!.Atch); SerializeTo(array, value._data!.Shp); + SerializeTo(array, value._data!.Atr); SerializeTo(array, value._data!); } @@ -839,6 +895,16 @@ public class MetaDictionary Penumbra.Log.Warning("Invalid SHP Manipulation encountered."); break; } + case MetaManipulationType.Atr: + { + var identifier = AtrIdentifier.FromJson(manip); + var entry = new AtrEntry(manip["Entry"]?.Value() ?? true); + if (identifier.HasValue) + dict.TryAdd(identifier.Value, entry); + else + Penumbra.Log.Warning("Invalid ATR Manipulation encountered."); + break; + } case MetaManipulationType.GlobalEqp: { var identifier = GlobalEqpManipulation.FromJson(manip); diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index 3be46d32..0a5b71b7 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; +using Penumbra.Collections.Cache; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -18,7 +19,12 @@ public enum ShapeConnectorCondition : byte Ankles = 3, } -public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeConnectorCondition ConnectorCondition) +public readonly record struct ShpIdentifier( + HumanSlot Slot, + PrimaryId? Id, + ShapeAttributeString Shape, + ShapeConnectorCondition ConnectorCondition, + GenderRace GenderRaceCondition) : IComparable, IMetaIdentifier { public int CompareTo(ShpIdentifier other) @@ -49,6 +55,10 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (conditionComparison is not 0) return conditionComparison; + var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition); + if (genderRaceComparison is not 0) + return genderRaceComparison; + return Shape.CompareTo(other.Shape); } @@ -80,6 +90,9 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break; } + if (GenderRaceCondition is not GenderRace.Unknown) + sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode()); + return sb.ToString(); } @@ -96,6 +109,12 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus) return false; + if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition)) + return false; + + if (!Enum.IsDefined(ConnectorCondition)) + return false; + if (Slot is HumanSlot.Unknown && Id is not null) return false; @@ -105,10 +124,7 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 }) return false; - if (!ValidateCustomShapeString(Shape)) - return false; - - if (!Enum.IsDefined(ConnectorCondition)) + if (!Shape.ValidateCustomShapeString()) return false; return ConnectorCondition switch @@ -121,40 +137,6 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape }; } - public static unsafe bool ValidateCustomShapeString(byte* shape) - { - // "shpx_*" - if (shape is null) - return false; - - if (*shape++ is not (byte)'s' - || *shape++ is not (byte)'h' - || *shape++ is not (byte)'p' - || *shape++ is not (byte)'x' - || *shape++ is not (byte)'_' - || *shape is 0) - return false; - - return true; - } - - public static bool ValidateCustomShapeString(in ShapeString shape) - { - // "shpx_*" - if (shape.Length < 6) - return false; - - var span = shape.AsSpan; - if (span[0] is not (byte)'s' - || span[1] is not (byte)'h' - || span[2] is not (byte)'p' - || span[3] is not (byte)'x' - || span[4] is not (byte)'_') - return false; - - return true; - } - public JObject AddToJson(JObject jObj) { if (Slot is not HumanSlot.Unknown) @@ -164,19 +146,22 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape jObj["Shape"] = Shape.ToString(); if (ConnectorCondition is not ShapeConnectorCondition.None) jObj["ConnectorCondition"] = ConnectorCondition.ToString(); + if (GenderRaceCondition is not GenderRace.Unknown) + jObj["GenderRaceCondition"] = (uint)GenderRaceCondition; return jObj; } public static ShpIdentifier? FromJson(JObject jObj) { var shape = jObj["Shape"]?.ToObject(); - if (shape is null || !ShapeString.TryRead(shape, out var shapeString)) + if (shape is null || !ShapeAttributeString.TryRead(shape, out var shapeString)) return null; - var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; - var id = jObj["Id"]?.ToObject(); - var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; - var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition); + var slot = jObj["Slot"]?.ToObject() ?? HumanSlot.Unknown; + var id = jObj["Id"]?.ToObject(); + var connectorCondition = jObj["ConnectorCondition"]?.ToObject() ?? ShapeConnectorCondition.None; + var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject() ?? 0; + var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition); return identifier.Validate() ? identifier : null; } diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs new file mode 100644 index 00000000..c6800141 --- /dev/null +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -0,0 +1,153 @@ +using OtterGui.Services; +using Penumbra.Collections; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; +using Penumbra.Interop.Hooks.PostProcessing; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Meta; + +public unsafe class ShapeAttributeManager : IRequiredService, IDisposable +{ + public const int NumSlots = 14; + public const int ModelSlotSize = 18; + private readonly AttributeHook _attributeHook; + + public static ReadOnlySpan UsedModels + => + [ + HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, + HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, + ]; + + public ShapeAttributeManager(AttributeHook attributeHook) + { + _attributeHook = attributeHook; + _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeAttributeManager); + } + + private readonly Dictionary[] _temporaryShapes = + Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); + + private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; + + private HumanSlot _modelIndex; + private int _slotIndex; + private GenderRace _genderRace; + + private FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* _model; + + public void Dispose() + => _attributeHook.Unsubscribe(OnAttributeComputed); + + private void OnAttributeComputed(Actor actor, Model model, ModCollection collection) + { + if (!collection.HasCache) + return; + + _genderRace = (GenderRace)model.AsHuman->RaceSexId; + for (_slotIndex = 0; _slotIndex < NumSlots; ++_slotIndex) + { + _modelIndex = UsedModels[_slotIndex]; + _model = model.AsHuman->Models[_modelIndex.ToIndex()]; + if (_model is null || _model->ModelResourceHandle is null) + continue; + + _ids[(int)_modelIndex] = model.GetModelId(_modelIndex); + CheckShapes(collection.MetaCache!.Shp); + CheckAttributes(collection.MetaCache!.Atr); + } + + UpdateDefaultMasks(model, collection.MetaCache!.Shp); + } + + private void CheckAttributes(AtrCache attributeCache) + { + if (attributeCache.DisabledCount is 0) + return; + + ref var attributes = ref _model->ModelResourceHandle->Attributes; + foreach (var (attribute, index) in attributes.Where(kvp => ShapeAttributeString.ValidateCustomAttributeString(kvp.Key.Value))) + { + if (ShapeAttributeString.TryRead(attribute.Value, out var attributeString)) + { + // Mask out custom attributes if they are disabled. Attributes are enabled by default. + if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) + _model->EnabledAttributeIndexMask &= (ushort)~(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a attribute string that is too long: {attribute}."); + } + } + } + + private void CheckShapes(ShpCache shapeCache) + { + _temporaryShapes[_slotIndex].Clear(); + ref var shapes = ref _model->ModelResourceHandle->Shapes; + foreach (var (shape, index) in shapes.Where(kvp => ShapeAttributeString.ValidateCustomShapeString(kvp.Key.Value))) + { + if (ShapeAttributeString.TryRead(shape.Value, out var shapeString)) + { + _temporaryShapes[_slotIndex].TryAdd(shapeString, index); + // Add custom shapes if they are enabled. Shapes are disabled by default. + if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) + _model->EnabledShapeKeyIndexMask |= (ushort)(1 << index); + } + else + { + Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); + } + } + } + + private void UpdateDefaultMasks(Model human, ShpCache cache) + { + foreach (var (shape, topIndex) in _temporaryShapes[1]) + { + if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex)) + { + human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; + human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); + } + + if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex)) + { + human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; + human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); + } + } + + foreach (var (shape, bottomIndex) in _temporaryShapes[3]) + { + if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex)) + { + human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex; + human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex; + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); + } + } + + return; + + void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, + HumanSlot slot2, int idx1, int idx2) + { + if (dict.Count is 0) + return; + + foreach (var (shape, set) in dict) + { + if (set.Contains(slot1, _ids[idx1], GenderRace.Unknown) && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) + human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1; + if (set.Contains(slot2, _ids[idx2], GenderRace.Unknown) && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) + human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2; + } + } + } +} diff --git a/Penumbra/Meta/ShapeString.cs b/Penumbra/Meta/ShapeAttributeString.cs similarity index 55% rename from Penumbra/Meta/ShapeString.cs rename to Penumbra/Meta/ShapeAttributeString.cs index 95ca0933..55e3f021 100644 --- a/Penumbra/Meta/ShapeString.cs +++ b/Penumbra/Meta/ShapeAttributeString.cs @@ -6,11 +6,11 @@ using Penumbra.String.Functions; namespace Penumbra.Meta; [JsonConverter(typeof(Converter))] -public struct ShapeString : IEquatable, IComparable +public struct ShapeAttributeString : IEquatable, IComparable { public const int MaxLength = 30; - public static readonly ShapeString Empty = new(); + public static readonly ShapeAttributeString Empty = new(); private FixedString32 _buffer; @@ -37,6 +37,72 @@ public struct ShapeString : IEquatable, IComparable } } + public static unsafe bool ValidateCustomShapeString(byte* shape) + { + // "shpx_*" + if (shape is null) + return false; + + if (*shape++ is not (byte)'s' + || *shape++ is not (byte)'h' + || *shape++ is not (byte)'p' + || *shape++ is not (byte)'x' + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public bool ValidateCustomShapeString() + { + // "shpx_*" + if (Length < 6) + return false; + + if (_buffer[0] is not (byte)'s' + || _buffer[1] is not (byte)'h' + || _buffer[2] is not (byte)'p' + || _buffer[3] is not (byte)'x' + || _buffer[4] is not (byte)'_') + return false; + + return true; + } + + public static unsafe bool ValidateCustomAttributeString(byte* shape) + { + // "atrx_*" + if (shape is null) + return false; + + if (*shape++ is not (byte)'a' + || *shape++ is not (byte)'t' + || *shape++ is not (byte)'r' + || *shape++ is not (byte)'x' + || *shape++ is not (byte)'_' + || *shape is 0) + return false; + + return true; + } + + public bool ValidateCustomAttributeString() + { + // "atrx_*" + if (Length < 6) + return false; + + if (_buffer[0] is not (byte)'a' + || _buffer[1] is not (byte)'t' + || _buffer[2] is not (byte)'r' + || _buffer[3] is not (byte)'x' + || _buffer[4] is not (byte)'_') + return false; + + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public bool IsAnkle() => CheckCenter('a', 'n'); @@ -53,28 +119,28 @@ public struct ShapeString : IEquatable, IComparable private bool CheckCenter(char first, char second) => Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_'; - public bool Equals(ShapeString other) + public bool Equals(ShapeAttributeString other) => Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]); public override bool Equals(object? obj) - => obj is ShapeString other && Equals(other); + => obj is ShapeAttributeString other && Equals(other); public override int GetHashCode() => (int)Crc32.Get(_buffer[..Length]); - public static bool operator ==(ShapeString left, ShapeString right) + public static bool operator ==(ShapeAttributeString left, ShapeAttributeString right) => left.Equals(right); - public static bool operator !=(ShapeString left, ShapeString right) + public static bool operator !=(ShapeAttributeString left, ShapeAttributeString right) => !left.Equals(right); - public static unsafe bool TryRead(byte* pointer, out ShapeString ret) + public static unsafe bool TryRead(byte* pointer, out ShapeAttributeString ret) { var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer); return TryRead(span, out ret); } - public unsafe int CompareTo(ShapeString other) + public unsafe int CompareTo(ShapeAttributeString other) { fixed (void* lhs = &this) { @@ -82,7 +148,7 @@ public struct ShapeString : IEquatable, IComparable } } - public static bool TryRead(ReadOnlySpan utf8, out ShapeString ret) + public static bool TryRead(ReadOnlySpan utf8, out ShapeAttributeString ret) { if (utf8.Length is 0 or > MaxLength) { @@ -97,7 +163,7 @@ public struct ShapeString : IEquatable, IComparable return true; } - public static bool TryRead(ReadOnlySpan utf16, out ShapeString ret) + public static bool TryRead(ReadOnlySpan utf16, out ShapeAttributeString ret) { ret = Empty; if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written)) @@ -116,19 +182,20 @@ public struct ShapeString : IEquatable, IComparable _buffer[31] = length; } - private sealed class Converter : JsonConverter + private sealed class Converter : JsonConverter { - public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, ShapeAttributeString value, JsonSerializer serializer) { writer.WriteValue(value.ToString()); } - public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue, + public override ShapeAttributeString ReadJson(JsonReader reader, Type objectType, ShapeAttributeString existingValue, + bool hasExistingValue, JsonSerializer serializer) { var value = serializer.Deserialize(reader); if (!TryRead(value, out existingValue)) - throw new JsonReaderException($"Could not parse {value} into ShapeString."); + throw new JsonReaderException($"Could not parse {value} into ShapeAttributeString."); return existingValue; } diff --git a/Penumbra/Meta/ShapeManager.cs b/Penumbra/Meta/ShapeManager.cs deleted file mode 100644 index abd4c3b8..00000000 --- a/Penumbra/Meta/ShapeManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using OtterGui.Services; -using Penumbra.Collections; -using Penumbra.Collections.Cache; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Interop; -using Penumbra.GameData.Structs; -using Penumbra.Interop.Hooks.PostProcessing; -using Penumbra.Meta.Manipulations; - -namespace Penumbra.Meta; - -public class ShapeManager : IRequiredService, IDisposable -{ - public const int NumSlots = 14; - public const int ModelSlotSize = 18; - private readonly AttributeHook _attributeHook; - - public static ReadOnlySpan UsedModels - => - [ - HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists, - HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear, - ]; - - public ShapeManager(AttributeHook attributeHook) - { - _attributeHook = attributeHook; - _attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager); - } - - private readonly Dictionary[] _temporaryIndices = - Enumerable.Range(0, NumSlots).Select(_ => new Dictionary()).ToArray(); - - private readonly uint[] _temporaryMasks = new uint[NumSlots]; - private readonly uint[] _temporaryValues = new uint[NumSlots]; - private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize]; - - public void Dispose() - => _attributeHook.Unsubscribe(OnAttributeComputed); - - private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection) - { - if (!collection.HasCache) - return; - - ComputeCache(model, collection.MetaCache!.Shp); - for (var i = 0; i < NumSlots; ++i) - { - if (_temporaryMasks[i] is 0) - continue; - - var modelIndex = UsedModels[i]; - var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask; - var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i]; - Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}."); - model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask; - } - } - - private unsafe void ComputeCache(Model human, ShpCache cache) - { - for (var i = 0; i < NumSlots; ++i) - { - _temporaryMasks[i] = 0; - _temporaryValues[i] = 0; - _temporaryIndices[i].Clear(); - - var modelIndex = UsedModels[i]; - var model = human.AsHuman->Models[modelIndex.ToIndex()]; - if (model is null || model->ModelResourceHandle is null) - continue; - - _ids[(int)modelIndex] = human.GetModelId(modelIndex); - - ref var shapes = ref model->ModelResourceHandle->Shapes; - foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value))) - { - if (ShapeString.TryRead(shape.Value, out var shapeString)) - { - _temporaryIndices[i].TryAdd(shapeString, index); - _temporaryMasks[i] |= (ushort)(1 << index); - if (cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex])) - _temporaryValues[i] |= (ushort)(1 << index); - } - else - { - Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}."); - } - } - } - - UpdateDefaultMasks(cache); - } - - private void UpdateDefaultMasks(ShpCache cache) - { - foreach (var (shape, topIndex) in _temporaryIndices[1]) - { - if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex)) - { - _temporaryValues[1] |= 1u << topIndex; - _temporaryValues[2] |= 1u << handIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); - } - - if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex)) - { - _temporaryValues[1] |= 1u << topIndex; - _temporaryValues[3] |= 1u << legIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); - } - } - - foreach (var (shape, bottomIndex) in _temporaryIndices[3]) - { - if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex)) - { - _temporaryValues[3] |= 1u << bottomIndex; - _temporaryValues[4] |= 1u << footIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); - } - } - - return; - - void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) - { - if (dict.Count is 0) - return; - - foreach (var (shape, set) in dict) - { - if (set.Contains(slot1, _ids[idx1]) && _temporaryIndices[idx1].TryGetValue(shape, out var index1)) - _temporaryValues[idx1] |= 1u << index1; - if (set.Contains(slot2, _ids[idx2]) && _temporaryIndices[idx2].TryGetValue(shape, out var index2)) - _temporaryValues[idx2] |= 1u << index2; - } - } - } -} diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs new file mode 100644 index 00000000..89fadfa8 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/Meta/AtrMetaDrawer.cs @@ -0,0 +1,274 @@ +using Dalamud.Interface; +using ImGuiNET; +using Newtonsoft.Json.Linq; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Collections.Cache; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.AdvancedWindow.Meta; + +public sealed class AtrMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) + : MetaDrawer(editor, metaFiles), IService +{ + public override ReadOnlySpan Label + => "Attributes(ATR)###ATR"u8; + + private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("atrx_"u8, out var s) ? s : ShapeAttributeString.Empty; + private bool _identifierValid; + + public override int NumColumns + => 7; + + public override float ColumnHeight + => ImUtf8.FrameHeightSpacing; + + protected override void Initialize() + { + Identifier = new AtrIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, GenderRace.Unknown); + Entry = AtrEntry.True; + } + + protected override void DrawNew() + { + ImGui.TableNextColumn(); + CopyToClipboardButton("Copy all current ATR manipulations to clipboard."u8, + new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atr))); + + ImGui.TableNextColumn(); + var canAdd = !Editor.Contains(Identifier) && _identifierValid; + var tt = canAdd + ? "Stage this edit."u8 + : _identifierValid + ? "This entry does not contain a valid attribute."u8 + : "This entry is already edited."u8; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) + Editor.Changes |= Editor.TryAdd(Identifier, AtrEntry.False); + + DrawIdentifierInput(ref Identifier); + DrawEntry(ref Entry, true); + } + + protected override void DrawEntry(AtrIdentifier identifier, AtrEntry entry) + { + DrawMetaButtons(identifier, entry); + DrawIdentifier(identifier); + + if (DrawEntry(ref entry, false)) + Editor.Changes |= Editor.Update(identifier, entry); + } + + protected override IEnumerable<(AtrIdentifier, AtrEntry)> Enumerate() + => Editor.Atr + .OrderBy(kvp => kvp.Key.Attribute) + .ThenBy(kvp => kvp.Key.Slot) + .ThenBy(kvp => kvp.Key.Id) + .Select(kvp => (kvp.Key, kvp.Value)); + + protected override int Count + => Editor.Atr.Count; + + private bool DrawIdentifierInput(ref AtrIdentifier identifier) + { + ImGui.TableNextColumn(); + var changes = DrawHumanSlot(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawGenderRaceConditionInput(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawPrimaryId(ref identifier); + + ImGui.TableNextColumn(); + changes |= DrawAttributeKeyInput(ref identifier, ref _buffer, ref _identifierValid); + return changes; + } + + private static void DrawIdentifier(AtrIdentifier identifier) + { + ImGui.TableNextColumn(); + + ImUtf8.TextFramed(ShpMetaDrawer.SlotName(identifier.Slot), FrameColor); + ImUtf8.HoverTooltip("Model Slot"u8); + + ImGui.TableNextColumn(); + if (identifier.GenderRaceCondition is not GenderRace.Unknown) + { + ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor); + ImUtf8.HoverTooltip("Gender & Race Code for this attribute to be set."); + } + else + { + ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor); + } + + ImGui.TableNextColumn(); + if (identifier.Id.HasValue) + ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); + else + ImUtf8.TextFramed("All IDs"u8, FrameColor); + ImUtf8.HoverTooltip("Primary ID"u8); + + ImGui.TableNextColumn(); + ImUtf8.TextFramed(identifier.Attribute.AsSpan, FrameColor); + } + + private static bool DrawEntry(ref AtrEntry entry, bool disabled) + { + using var dis = ImRaii.Disabled(disabled); + ImGui.TableNextColumn(); + var value = entry.Value; + var changes = ImUtf8.Checkbox("##atrEntry"u8, ref value); + if (changes) + entry = new AtrEntry(value); + ImUtf8.HoverTooltip("Whether to enable or disable this attribute for the selected items."); + return changes; + } + + public static bool DrawPrimaryId(ref AtrIdentifier identifier, float unscaledWidth = 100) + { + var allSlots = identifier.Slot is HumanSlot.Unknown; + var all = !identifier.Id.HasValue; + var ret = false; + using (ImRaii.Disabled(allSlots)) + { + if (ImUtf8.Checkbox("##atrAll"u8, ref all)) + { + identifier = identifier with { Id = all ? null : 0 }; + ret = true; + } + } + + ImUtf8.HoverTooltip(allSlots + ? "When using all slots, you also need to use all IDs."u8 + : "Enable this attribute for all model IDs."u8); + + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + if (all) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.05f, 0.5f)); + ImUtf8.TextFramed("All IDs"u8, ImGui.GetColorU32(ImGuiCol.FrameBg, all || allSlots ? ImGui.GetStyle().DisabledAlpha : 1f), + new Vector2(unscaledWidth, 0), ImGui.GetColorU32(ImGuiCol.TextDisabled)); + } + else + { + var max = identifier.Slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1; + if (IdInput("##atrPrimaryId"u8, unscaledWidth, identifier.Id.GetValueOrDefault(0).Id, out var setId, 0, max, false)) + { + identifier = identifier with { Id = setId }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Primary ID - You can usually find this as the 'e####' part of an item path or similar for customizations."u8); + + return ret; + } + + public bool DrawHumanSlot(ref AtrIdentifier identifier, float unscaledWidth = 150) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + using (var combo = ImUtf8.Combo("##atrSlot"u8, ShpMetaDrawer.SlotName(identifier.Slot))) + { + if (combo) + foreach (var slot in ShpMetaDrawer.AvailableSlots) + { + if (!ImUtf8.Selectable(ShpMetaDrawer.SlotName(slot), slot == identifier.Slot) || slot == identifier.Slot) + continue; + + ret = true; + if (slot is HumanSlot.Unknown) + { + identifier = identifier with + { + Id = null, + Slot = slot, + }; + } + else + { + identifier = identifier with + { + Id = identifier.Id.HasValue + ? (PrimaryId)Math.Clamp(identifier.Id.Value.Id, 0, + slot.ToSpecificEnum() is BodySlot ? byte.MaxValue : ExpandedEqpGmpBase.Count - 1) + : null, + Slot = slot, + }; + ret = true; + } + } + } + + ImUtf8.HoverTooltip("Model Slot"u8); + return ret; + } + + private static bool DrawGenderRaceConditionInput(ref AtrIdentifier identifier, float unscaledWidth = 250) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + + using (var combo = ImUtf8.Combo("##shpGenderRace"u8, + identifier.GenderRaceCondition is GenderRace.Unknown + ? "Any Gender & Race" + : $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})")) + { + if (combo) + { + if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown) + && identifier.GenderRaceCondition is not GenderRace.Unknown) + { + identifier = identifier with { GenderRaceCondition = GenderRace.Unknown }; + ret = true; + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr) + && identifier.GenderRaceCondition != gr) + { + identifier = identifier with { GenderRaceCondition = gr }; + ret = true; + } + } + } + } + + ImUtf8.HoverTooltip( + "Only activate this attribute for this gender & race code."u8); + + return ret; + } + + public static unsafe bool DrawAttributeKeyInput(ref AtrIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid, + float unscaledWidth = 150) + { + var ret = false; + var ptr = Unsafe.AsPointer(ref buffer); + var span = new Span(ptr, ShapeAttributeString.MaxLength + 1); + using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) + { + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + if (ImUtf8.InputText("##atrAttribute"u8, span, out int newLength, "Attribute..."u8)) + { + buffer.ForceLength((byte)newLength); + valid = buffer.ValidateCustomAttributeString(); + if (valid) + identifier = identifier with { Attribute = buffer }; + ret = true; + } + } + + ImUtf8.HoverTooltip("Supported attribute need to have the format `atrx_*` and a maximum length of 30 characters."u8); + return ret; + } +} diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs index 70b5f83b..792611e2 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawers.cs @@ -12,7 +12,8 @@ public class MetaDrawers( ImcMetaDrawer imc, RspMetaDrawer rsp, AtchMetaDrawer atch, - ShpMetaDrawer shp) : IService + ShpMetaDrawer shp, + AtrMetaDrawer atr) : IService { public readonly EqdpMetaDrawer Eqdp = eqdp; public readonly EqpMetaDrawer Eqp = eqp; @@ -23,6 +24,7 @@ public class MetaDrawers( public readonly GlobalEqpMetaDrawer GlobalEqp = globalEqp; public readonly AtchMetaDrawer Atch = atch; public readonly ShpMetaDrawer Shp = shp; + public readonly AtrMetaDrawer Atr = atr; public IMetaDrawer? Get(MetaManipulationType type) => type switch @@ -35,6 +37,7 @@ public class MetaDrawers( MetaManipulationType.Rsp => Rsp, MetaManipulationType.Atch => Atch, MetaManipulationType.Shp => Shp, + MetaManipulationType.Atr => Atr, MetaManipulationType.GlobalEqp => GlobalEqp, _ => null, }; diff --git a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs index 6505ecc0..35c8ccec 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/ShpMetaDrawer.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; +using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -20,18 +21,18 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile public override ReadOnlySpan Label => "Shape Keys (SHP)###SHP"u8; - private ShapeString _buffer = ShapeString.TryRead("shpx_"u8, out var s) ? s : ShapeString.Empty; - private bool _identifierValid; + private ShapeAttributeString _buffer = ShapeAttributeString.TryRead("shpx_"u8, out var s) ? s : ShapeAttributeString.Empty; + private bool _identifierValid; public override int NumColumns - => 7; + => 8; public override float ColumnHeight => ImUtf8.FrameHeightSpacing; protected override void Initialize() { - Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeString.Empty, ShapeConnectorCondition.None); + Identifier = new ShpIdentifier(HumanSlot.Unknown, null, ShapeAttributeString.Empty, ShapeConnectorCondition.None, GenderRace.Unknown); } protected override void DrawNew() @@ -79,6 +80,9 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImGui.TableNextColumn(); var changes = DrawHumanSlot(ref identifier); + ImGui.TableNextColumn(); + changes |= DrawGenderRaceConditionInput(ref identifier); + ImGui.TableNextColumn(); changes |= DrawPrimaryId(ref identifier); @@ -97,6 +101,17 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile ImUtf8.TextFramed(SlotName(identifier.Slot), FrameColor); ImUtf8.HoverTooltip("Model Slot"u8); + ImGui.TableNextColumn(); + if (identifier.GenderRaceCondition is not GenderRace.Unknown) + { + ImUtf8.TextFramed($"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})", FrameColor); + ImUtf8.HoverTooltip("Gender & Race Code for this shape key to be set."); + } + else + { + ImUtf8.TextFramed("Any Gender & Race"u8, FrameColor); + } + ImGui.TableNextColumn(); if (identifier.Id.HasValue) ImUtf8.TextFramed($"{identifier.Id.Value.Id}", FrameColor); @@ -165,7 +180,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 150) + public bool DrawHumanSlot(ref ShpIdentifier identifier, float unscaledWidth = 170) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -212,18 +227,19 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeString buffer, ref bool valid, float unscaledWidth = 150) + public static unsafe bool DrawShapeKeyInput(ref ShpIdentifier identifier, ref ShapeAttributeString buffer, ref bool valid, + float unscaledWidth = 200) { var ret = false; var ptr = Unsafe.AsPointer(ref buffer); - var span = new Span(ptr, ShapeString.MaxLength + 1); + var span = new Span(ptr, ShapeAttributeString.MaxLength + 1); using (new ImRaii.ColorStyle().Push(ImGuiCol.Border, Colors.RegexWarningBorder, !valid).Push(ImGuiStyleVar.FrameBorderSize, 1f, !valid)) { ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); if (ImUtf8.InputText("##shpShape"u8, span, out int newLength, "Shape Key..."u8)) { buffer.ForceLength((byte)newLength); - valid = ShpIdentifier.ValidateCustomShapeString(buffer); + valid = buffer.ValidateCustomShapeString(); if (valid) identifier = identifier with { Shape = buffer }; ret = true; @@ -234,7 +250,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - public static unsafe bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 150) + private static bool DrawConnectorConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 80) { var ret = false; ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); @@ -271,7 +287,43 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile return ret; } - private static ReadOnlySpan AvailableSlots + private static bool DrawGenderRaceConditionInput(ref ShpIdentifier identifier, float unscaledWidth = 250) + { + var ret = false; + ImGui.SetNextItemWidth(unscaledWidth * ImUtf8.GlobalScale); + + using (var combo = ImUtf8.Combo("##shpGenderRace"u8, identifier.GenderRaceCondition is GenderRace.Unknown + ? "Any Gender & Race" + : $"{identifier.GenderRaceCondition.ToName()} ({identifier.GenderRaceCondition.ToRaceCode()})")) + { + if (combo) + { + if (ImUtf8.Selectable("Any Gender & Race"u8, identifier.GenderRaceCondition is GenderRace.Unknown) + && identifier.GenderRaceCondition is not GenderRace.Unknown) + { + identifier = identifier with { GenderRaceCondition = GenderRace.Unknown }; + ret = true; + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (ImUtf8.Selectable($"{gr.ToName()} ({gr.ToRaceCode()})", identifier.GenderRaceCondition == gr) + && identifier.GenderRaceCondition != gr) + { + identifier = identifier with { GenderRaceCondition = gr }; + ret = true; + } + } + } + } + + ImUtf8.HoverTooltip( + "Only activate this shape key for this gender & race code."u8); + + return ret; + } + + public static ReadOnlySpan AvailableSlots => [ HumanSlot.Unknown, @@ -291,7 +343,7 @@ public sealed class ShpMetaDrawer(ModMetaEditor editor, MetaFileManager metaFile HumanSlot.Ear, ]; - private static ReadOnlySpan SlotName(HumanSlot slot) + public static ReadOnlySpan SlotName(HumanSlot slot) => slot switch { HumanSlot.Unknown => "All Slots"u8, diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 70a15373..3f19da5e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -62,6 +62,7 @@ public partial class ModEditWindow DrawEditHeader(MetaManipulationType.Rsp); DrawEditHeader(MetaManipulationType.Atch); DrawEditHeader(MetaManipulationType.Shp); + DrawEditHeader(MetaManipulationType.Atr); DrawEditHeader(MetaManipulationType.GlobalEqp); } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 109cb5c4..9290e52d 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -35,6 +35,27 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) DrawCollectionShapeCache(actor); DrawCharacterShapes(human); + DrawCollectionAttributeCache(actor); + DrawCharacterAttributes(human); + } + + private unsafe void DrawCollectionAttributeCache(Actor actor) + { + var data = resolver.IdentifyCollection(actor.AsObject, true); + using var treeNode1 = ImUtf8.TreeNode($"Collection Attribute Cache ({data.ModCollection})"); + if (!treeNode1.Success || !data.ModCollection.HasCache) + return; + + using var table = ImUtf8.Table("##aCache"u8, 2, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data) + DrawShapeAttribute(attribute, set); } private unsafe void DrawCollectionShapeCache(Actor actor) @@ -44,7 +65,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode1.Success || !data.ModCollection.HasCache) return; - using var table = ImUtf8.Table("##cacheTable"u8, 3, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##sCache"u8, 3, ImGuiTableFlags.RowBg); if (!table) return; @@ -58,14 +79,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) { ImUtf8.DrawTableColumn(condition.ToString()); - DrawShape(shape, set); + DrawShapeAttribute(shape, set); } } } - private static void DrawShape(in ShapeString shape, ShpCache.ShpHashSet set) + private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) { - ImUtf8.DrawTableColumn(shape.AsSpan); + ImUtf8.DrawTableColumn(shapeAttribute.AsSpan); if (set.All) { ImUtf8.DrawTableColumn("All"u8); @@ -73,7 +94,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) else { ImGui.TableNextColumn(); - foreach (var slot in ShapeManager.UsedModels) + foreach (var slot in ShapeAttributeManager.UsedModels) { if (!set[slot]) continue; @@ -82,10 +103,52 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.SameLine(0, 0); } - foreach (var item in set.Where(i => !set[i.Slot])) + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) { - ImUtf8.Text($"{item.Slot.ToName()} {item.Id.Id:D4}, "); - ImGui.SameLine(0, 0); + if (set[gr]) + { + ImUtf8.Text($"All {gr.ToName()}, "); + ImGui.SameLine(0, 0); + } + else + { + foreach (var slot in ShapeAttributeManager.UsedModels) + { + if (!set[slot, gr]) + continue; + + ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + } + } + + + foreach (var ((slot, id), flags) in set) + { + if ((flags & 1ul) is not 0) + { + if (set[slot]) + continue; + + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + else + { + var currentFlags = flags >> 1; + var currentIndex = BitOperations.TrailingZeroCount(currentFlags); + while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) + { + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr]) + continue; + + ImUtf8.Text($"{gr.ToName()} {slot.ToName()} {id.Id:D4}, "); + currentFlags >>= currentIndex; + currentIndex = BitOperations.TrailingZeroCount(currentFlags); + } + } } } } @@ -96,7 +159,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##table"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 5, ImGuiTableFlags.RowBg); if (!table) return; @@ -140,4 +203,55 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) } } } + + private unsafe void DrawCharacterAttributes(Model human) + { + using var treeNode2 = ImUtf8.TreeNode("Character Model Attributes"u8); + if (!treeNode2) + return; + + using var table = ImUtf8.Table("##attributes"u8, 5, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImUtf8.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 25 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); + ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableHeadersRow(); + + var disabledColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + for (var i = 0; i < human.AsHuman->SlotCount; ++i) + { + ImUtf8.DrawTableColumn($"{(uint)i:D2}"); + ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + + ImGui.TableNextColumn(); + var model = human.AsHuman->Models[i]; + Penumbra.Dynamis.DrawPointer((nint)model); + if (model is not null) + { + var mask = model->EnabledAttributeIndexMask; + ImUtf8.DrawTableColumn($"{mask:X8}"); + ImGui.TableNextColumn(); + foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) + { + var disabled = (mask & (1u << idx)) is 0; + using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); + ImUtf8.Text(attribute.AsSpan()); + ImGui.SameLine(0, 0); + ImUtf8.Text(", "u8); + if (idx % 8 < 7) + ImGui.SameLine(0, 0); + } + } + else + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + } + } + } } diff --git a/schemas/structs/manipulation.json b/schemas/structs/manipulation.json index 55fc5cad..81f2cef3 100644 --- a/schemas/structs/manipulation.json +++ b/schemas/structs/manipulation.json @@ -3,7 +3,7 @@ "type": "object", "properties": { "Type": { - "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp" ] + "enum": [ "Unknown", "Imc", "Eqdp", "Eqp", "Est", "Gmp", "Rsp", "GlobalEqp", "Atch", "Shp", "Atr" ] }, "Manipulation": { "type": "object" @@ -100,6 +100,16 @@ "$ref": "meta_shp.json" } } + }, + { + "properties": { + "Type": { + "const": "Atr" + }, + "Manipulation": { + "$ref": "meta_atr.json" + } + } } ] } diff --git a/schemas/structs/meta_atr.json b/schemas/structs/meta_atr.json new file mode 100644 index 00000000..479d4127 --- /dev/null +++ b/schemas/structs/meta_atr.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "Entry": { + "type": "boolean" + }, + "Slot": { + "$ref": "meta_enums.json#HumanSlot" + }, + "Id": { + "$ref": "meta_enums.json#U16" + }, + "Attribute": { + "type": "string", + "minLength": 5, + "maxLength": 30, + "pattern": "^atrx_" + }, + "GenderRaceCondition": { + "enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ] + } + }, + "required": [ + "Attribute" + ] +} diff --git a/schemas/structs/meta_shp.json b/schemas/structs/meta_shp.json index 851842a4..cb7fd0ec 100644 --- a/schemas/structs/meta_shp.json +++ b/schemas/structs/meta_shp.json @@ -19,6 +19,9 @@ }, "ConnectorCondition": { "$ref": "meta_enums.json#ShapeConnectorCondition" + }, + "GenderRaceCondition": { + "enum": [ 0, 101, 201, 301, 401, 501, 601, 701, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, 1601, 1701, 1801 ] } }, "required": [ From f5db888bbdb1e5193086027daf3f99da95636c33 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 21 May 2025 13:49:29 +0000 Subject: [PATCH 704/865] [CI] Updating repo.json for testing_1.3.6.13 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 0413b876..3e2a9d2c 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.12", + "TestingAssemblyVersion": "1.3.6.13", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.13/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 507b0a5aee46b0c7a62e2a3a8ba997a0dbc4944e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 21 May 2025 18:07:12 +0200 Subject: [PATCH 705/865] Slight description update. --- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index cb22b54a..4bbdf2a9 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -761,7 +761,7 @@ 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 Support", "Penumbra will allow for custom shape keys for modded models to be considered and combined.", + 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(); From ac4c75d3c36f658c054d2ef508d909f94f975e39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 May 2025 11:13:42 +0200 Subject: [PATCH 706/865] Fix not updating meta count correctly. --- 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 23eaec76..ede062ae 100644 --- a/Penumbra/Meta/Manipulations/MetaDictionary.cs +++ b/Penumbra/Meta/Manipulations/MetaDictionary.cs @@ -596,6 +596,7 @@ public class MetaDictionary + _data!.Gmp.Count + _data!.Atch.Count + _data!.Shp.Count + + _data!.Atr.Count + _data!.Count; } From 400d7d0bea0a611be2071d56236ba3745828f4ab Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 May 2025 11:13:58 +0200 Subject: [PATCH 707/865] Slight improvements. --- Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs | 12 ++++++------ Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 10 ++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 3f19da5e..aa3d9172 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -165,7 +165,7 @@ public partial class ModEditWindow private void AddFromClipboardButton() { - if (ImGui.Button("Add from Clipboard")) + if (ImUtf8.Button("Add from Clipboard"u8)) { var clipboard = ImGuiUtil.GetClipboardText(); @@ -176,13 +176,13 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip( - "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations."); + ImUtf8.HoverTooltip( + "Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations."u8); } private void SetFromClipboardButton() { - if (ImGui.Button("Set from Clipboard")) + if (ImUtf8.Button("Set from Clipboard"u8)) { var clipboard = ImGuiUtil.GetClipboardText(); if (MetaApi.ConvertManips(clipboard, out var manips, out _)) @@ -192,7 +192,7 @@ public partial class ModEditWindow } } - ImGuiUtil.HoverTooltip( - "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."); + ImUtf8.HoverTooltip( + "Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations."u8); } } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 9290e52d..3180a212 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -159,7 +159,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##shapes"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 6, ImGuiTableFlags.RowBg); if (!table) return; @@ -167,6 +167,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -184,6 +185,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledShapeKeyIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); ImGui.TableNextColumn(); foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) { @@ -200,6 +202,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } @@ -210,7 +213,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##attributes"u8, 5, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##attributes"u8, 6, ImGuiTableFlags.RowBg); if (!table) return; @@ -218,6 +221,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -235,6 +239,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledAttributeIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); ImGui.TableNextColumn(); foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) { @@ -251,6 +256,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } From bc4f88aee9e91b299a828e18b1196c6562df13e8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 22 May 2025 11:14:17 +0200 Subject: [PATCH 708/865] Fix shape/attribute mask stupidity. --- Penumbra/Meta/ShapeAttributeManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index c6800141..e9c9c169 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -75,7 +75,7 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable { // Mask out custom attributes if they are disabled. Attributes are enabled by default. if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) - _model->EnabledAttributeIndexMask &= (ushort)~(1 << index); + _model->EnabledAttributeIndexMask &= ~(1u << index); } else { @@ -95,7 +95,7 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable _temporaryShapes[_slotIndex].TryAdd(shapeString, index); // Add custom shapes if they are enabled. Shapes are disabled by default. if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace)) - _model->EnabledShapeKeyIndexMask |= (ushort)(1 << index); + _model->EnabledShapeKeyIndexMask |= 1u << index; } else { From 9e7c30455625e9296702f461a7bf644a10af934a Mon Sep 17 00:00:00 2001 From: Actions User Date: Thu, 22 May 2025 09:16:22 +0000 Subject: [PATCH 709/865] [CI] Updating repo.json for testing_1.3.6.14 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 3e2a9d2c..8b262136 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.13", + "TestingAssemblyVersion": "1.3.6.14", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.13/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.14/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 1bdbfe22c1f7b576cdb25bd775fa437ded0db559 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 10:50:04 +0200 Subject: [PATCH 710/865] Update Libraries. --- OtterGui | 2 +- Penumbra.GameData | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OtterGui b/OtterGui index 9aeda9a8..421874a1 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 9aeda9a892d9b971e32b10db21a8daf9c0b9ee53 +Subproject commit 421874a12540b7f8c1279dcc6a92e895a94d2fbc diff --git a/Penumbra.GameData b/Penumbra.GameData index bb3b462b..14b3641f 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bb3b462bbc5bc2a598c1ad8c372b0cb255551fe1 +Subproject commit 14b3641f0fb520cb829ceb50fa7cb31255a1da4e From 08c91248583bdd07f98968f66de0a57bc1a58dea Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 11:29:52 +0200 Subject: [PATCH 711/865] Fix issue with shapes/attributes not checking the groups correctly. --- .../Cache/ShapeAttributeHashSet.cs | 70 +++++++++++-------- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 11 +-- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index f1fc7127..74691e41 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -21,43 +21,39 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); - public bool this[HumanSlot slot] - => slot is HumanSlot.Unknown ? All : _allIds[(int)slot * GenderRaceIndices.Count]; - - public bool this[GenderRace genderRace] - => GenderRaceIndices.TryGetValue(genderRace, out var index) - && _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]; - - public bool this[HumanSlot slot, GenderRace genderRace] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private bool CheckGroups(HumanSlot slot, GenderRace genderRace) { - get - { - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return false; + if (All || this[slot]) + return true; - if (_allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count + index]) - return true; + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return false; - return _allIds[(int)slot * GenderRaceIndices.Count + index]; - } - set - { - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return; + if (_allIds[ToIndex(HumanSlot.Unknown, index)]) + return true; - var genderRaceCount = GenderRaceValues.Count; - if (slot is HumanSlot.Unknown) - _allIds[ShapeAttributeManager.ModelSlotSize * genderRaceCount + index] = value; - else - _allIds[(int)slot * genderRaceCount + index] = value; - } + return _allIds[ToIndex(slot, index)]; } + public bool this[HumanSlot slot] + => _allIds[ToIndex(slot, 0)]; + + public bool this[GenderRace genderRace] + => ToIndex(HumanSlot.Unknown, genderRace, out var index) && _allIds[index]; + + public bool this[HumanSlot slot, GenderRace genderRace] + => ToIndex(slot, genderRace, out var index) && _allIds[index]; + public bool All - => _allIds[ShapeAttributeManager.ModelSlotSize * GenderRaceIndices.Count]; + => _allIds[AllIndex]; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static int ToIndex(HumanSlot slot, int genderRaceIndex) + => slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count; public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => All || this[slot, genderRace] || ContainsEntry(slot, id, genderRace); + => CheckGroups(slot, genderRace) || ContainsEntry(slot, id, genderRace); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) @@ -72,9 +68,9 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI if (!id.HasValue) { - var slotIndex = slot is HumanSlot.Unknown ? ShapeAttributeManager.ModelSlotSize : (int)slot; - var old = _allIds[slotIndex * GenderRaceIndices.Count + index]; - _allIds[slotIndex * GenderRaceIndices.Count + index] = value; + var slotIndex = ToIndex(slot, index); + var old = _allIds[slotIndex]; + _allIds[slotIndex] = value; return old != value; } @@ -120,4 +116,16 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI public bool IsEmpty => !_allIds.HasAnySet() && Count is 0; + + private static readonly int AllIndex = ShapeAttributeManager.ModelSlotSize * GenderRaceValues.Count; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool ToIndex(HumanSlot slot, GenderRace genderRace, out int index) + { + if (!GenderRaceIndices.TryGetValue(genderRace, out index)) + return false; + + index = ToIndex(slot, index); + return true; + } } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 3180a212..fd37bf35 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -1,6 +1,7 @@ using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using ImGuiNET; +using OtterGui.Extensions; using OtterGui.Services; using OtterGui.Text; using Penumbra.Collections.Cache; @@ -167,7 +168,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); - ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); + ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); @@ -187,9 +188,9 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.DrawTableColumn($"{mask:X8}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); ImGui.TableNextColumn(); - foreach (var (shape, idx) in model->ModelResourceHandle->Shapes) + foreach (var ((shape, flag), idx) in model->ModelResourceHandle->Shapes.WithIndex()) { - var disabled = (mask & (1u << idx)) is 0; + var disabled = (mask & (1u << flag)) is 0; using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); ImUtf8.Text(shape.AsSpan()); ImGui.SameLine(0, 0); @@ -241,9 +242,9 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.DrawTableColumn($"{mask:X8}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); ImGui.TableNextColumn(); - foreach (var (attribute, idx) in model->ModelResourceHandle->Attributes) + foreach (var ((attribute, flag), idx) in model->ModelResourceHandle->Attributes.WithIndex()) { - var disabled = (mask & (1u << idx)) is 0; + var disabled = (mask & (1u << flag)) is 0; using var color = ImRaii.PushColor(ImGuiCol.Text, disabledColor, disabled); ImUtf8.Text(attribute.AsSpan()); ImGui.SameLine(0, 0); From ccc2c1fd4c9a226758dbb65d1330ae844fe21a80 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 11:30:10 +0200 Subject: [PATCH 712/865] Fix missing other option notifications for shp/atr. --- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 050dab51..b4db457d 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -64,6 +64,8 @@ public class ModMetaEditor( OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est)); OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp)); OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch)); + OtherData[MetaManipulationType.Shp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Shp)); + OtherData[MetaManipulationType.Atr].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atr)); OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp)); } From cd56163b1b75132b8892e462e1d36225a4a7d79c Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 23 May 2025 09:32:11 +0000 Subject: [PATCH 713/865] [CI] Updating repo.json for testing_1.3.6.15 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 8b262136..9a41e09b 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.3.6.8", - "TestingAssemblyVersion": "1.3.6.14", + "TestingAssemblyVersion": "1.3.6.15", "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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.14/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.15/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } From 82fc334be7c45921102d621a0dfee6ac8b93bc39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 23 May 2025 15:17:13 +0200 Subject: [PATCH 714/865] Use dynamis for some pointers. --- Penumbra/UI/Tabs/Debug/DebugTab.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b4fa3b9f..76df5acc 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1075,14 +1075,14 @@ public class DebugTab : Window, ITab, IUiService ImGui.TableNextColumn(); ImGui.TextUnformatted($"Slot {i}"); ImGui.TableNextColumn(); - ImGui.TextUnformatted(imc == null ? "NULL" : $"0x{(ulong)imc:X}"); + Penumbra.Dynamis.DrawPointer((nint)imc); ImGui.TableNextColumn(); if (imc != null) UiHelpers.Text(imc); var mdl = (RenderModel*)model->Models[i]; ImGui.TableNextColumn(); - ImGui.TextUnformatted(mdl == null ? "NULL" : $"0x{(ulong)mdl:X}"); + Penumbra.Dynamis.DrawPointer((nint)mdl); if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) continue; From ebe45c6a47eeea7cab7e7db38d2da90bb875304b Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 27 May 2025 11:33:10 +0200 Subject: [PATCH 715/865] Update Lib. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 421874a1..cee50c3f 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 421874a12540b7f8c1279dcc6a92e895a94d2fbc +Subproject commit cee50c3fe97a03ca7445c81de651b609620da526 From 2c115eda9426e53ffbcd06b494077b5863b17f3d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 May 2025 13:54:23 +0200 Subject: [PATCH 716/865] Slightly improve error message when importing wrongly named atch files. --- .../UI/AdvancedWindow/Meta/AtchMetaDrawer.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs index 5bc70fc3..5b6d585a 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/AtchMetaDrawer.cs @@ -34,6 +34,7 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer private AtchFile? _currentBaseAtchFile; private AtchPoint? _currentBaseAtchPoint; private readonly AtchPointCombo _combo; + private string _fileImport = string.Empty; public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : base(editor, metaFiles) @@ -48,6 +49,8 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer => obj.ToName(); } + private sealed class RaceCodeException(string filePath) : Exception($"Could not identify race code from path {filePath}."); + public void ImportFile(string filePath) { try @@ -57,14 +60,15 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer var gr = Parser.ParseRaceCode(filePath); if (gr is GenderRace.Unknown) - throw new Exception($"Could not identify race code from path {filePath}."); - var text = File.ReadAllBytes(filePath); - var file = new AtchFile(text); + throw new RaceCodeException(filePath); + + var text = File.ReadAllBytes(filePath); + var file = new AtchFile(text); foreach (var point in file.Points) { foreach (var (entry, index) in point.Entries.WithIndex()) { - var identifier = new AtchIdentifier(point.Type, gr, (ushort) index); + var identifier = new AtchIdentifier(point.Type, gr, (ushort)index); var defaultValue = AtchCache.GetDefault(MetaFiles, identifier); if (defaultValue == null) continue; @@ -76,6 +80,12 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer } } } + catch (RaceCodeException ex) + { + Penumbra.Messager.AddMessage(new Notification(ex, "The imported .atch file does not contain a race code (cXXXX) in its name.", + "Could not import .atch file:", + NotificationType.Warning)); + } catch (Exception ex) { Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:", @@ -157,12 +167,12 @@ public sealed class AtchMetaDrawer : MetaDrawer, ISer private void UpdateFile() { - _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; + _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; _currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type); if (_currentBaseAtchPoint == null) { _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); - Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; + Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; } if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length) From 5e985f4a84b45706373db655bcc4472917d4d10a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 28 May 2025 13:54:42 +0200 Subject: [PATCH 717/865] 1.4.0.0 --- Penumbra/UI/Changelog.cs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs index 5e1612eb..c1f7a1e6 100644 --- a/Penumbra/UI/Changelog.cs +++ b/Penumbra/UI/Changelog.cs @@ -61,10 +61,49 @@ public class PenumbraChangelog : IUiService Add1_3_5_0(Changelog); Add1_3_6_0(Changelog); Add1_3_6_4(Changelog); + Add1_4_0_0(Changelog); } #region Changelogs + 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!).") + .RegisterEntry("Those allow mod creators to toggle custom shape keys and attributes for models on and off, respectively.", 1) + .RegisterEntry("Custom shape keys need to have the format 'shpx_*' and custom attributes need 'atrx_*'.", 1) + .RegisterHighlight( + "Shapes of the following formats will automatically be toggled on if both relevant slots contain the same shape key:", 1) + .RegisterEntry("'shpx_wa_*', for the waist seam between the body and leg slot,", 2) + .RegisterEntry("'shpx_wr_*', for the wrist seams between the body and hands slot,", 2) + .RegisterEntry("'shpx_an_*', for the ankle seams between the leg and feet slot.", 2) + .RegisterEntry( + "Custom shape key and attributes can be turned off in the advanced settings section for the moment, but this is not recommended.", + 1) + .RegisterHighlight("The mod selector width is now draggable within certain restrictions that depend on the total window width.") + .RegisterEntry("The current behavior may not be final, let me know if you have any comments.", 1) + .RegisterEntry("Improved the naming of NPCs for identifiers by using Haselnussbombers new naming functionality (Thanks Hasel!).") + .RegisterEntry("Added global EQP entries to always hide Au Ra horns, Viera ears, or Miqo'te ears, respectively.") + .RegisterEntry("This will leave holes in the heads of the respective race if not modded in some way.", 1) + .RegisterEntry("Added a filter for mods that have temporary settings in the mod selector panel (Thanks Caraxi).") + .RegisterEntry("Made the checkbox for toggling Temporary Settings Mode in the mod tab more visible.") + .RegisterEntry("Improved the option select combo in advanced editing.") + .RegisterEntry("Fixed some issues with item identification for EST changes.") + .RegisterEntry("Fixed the sizing of the mod panel being off by 1 pixel sometimes.") + .RegisterEntry("Fixed an issue with redrawing while in GPose when other plugins broke some assumptions about the game state.") + .RegisterEntry("Fixed a clipping issue within the Meta Manipulations tab in advanced editing.") + .RegisterEntry("Fixed an issue with empty and temporary settings.") + .RegisterHighlight( + "In the Item Swap tab, items changed by this mod are now sorted and highlighted before items changed in the current collection before other items for the source, and inversely for the target. (1.3.6.8)") + .RegisterHighlight( + "Default-valued meta edits should now be kept on import and only removed when the option to keep them is not set AND no other options in the mod edit the same entry. (1.3.6.8)") + .RegisterEntry("Added a right-click context menu on file redirections to copy the full file path. (1.3.6.8)") + .RegisterEntry( + "Added a right-click context menu on the mod export button to open the backup directory in your file explorer. (1.3.6.8)") + .RegisterEntry("Fixed some issues when redrawing characters from other plugins. (1.3.6.8)") + .RegisterEntry( + "Added a modifier key separate from the delete modifier key that is used for less important key-checks, specifically toggling incognito mode. (1.3.6.7)") + .RegisterEntry("Fixed some issues with the Material Editor (Thanks Ny). (1.3.6.6)"); + private static void Add1_3_6_4(Changelog log) => log.NextVersion("Version 1.3.6.4") .RegisterEntry("The material editor should be functional again."); From 1551d9b6f3fa35ffde863e5442eab23ea52c6e35 Mon Sep 17 00:00:00 2001 From: Actions User Date: Wed, 28 May 2025 11:56:49 +0000 Subject: [PATCH 718/865] [CI] Updating repo.json for 1.4.0.0 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 9a41e09b..94020c96 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.3.6.8", - "TestingAssemblyVersion": "1.3.6.15", + "AssemblyVersion": "1.4.0.0", + "TestingAssemblyVersion": "1.4.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.3.6.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.6.15/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.6.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From f2927290f52ceebde1a60edd2f5b8b25cf56e186 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 30 May 2025 14:35:13 +0200 Subject: [PATCH 719/865] Fix exceptions when unsubscribing during event invocation. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index cee50c3f..17a3ee57 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit cee50c3fe97a03ca7445c81de651b609620da526 +Subproject commit 17a3ee5711ca30eb7f5b393dfb8136f0bce49b2b From ff2a9f95c46a7500528127aaca0c413653df706e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 30 May 2025 14:36:07 +0200 Subject: [PATCH 720/865] Fix Atr and Shp not being transmitted via Mare, add improved compression but don't use it yet. --- Penumbra/Api/Api/MetaApi.cs | 232 ++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/Penumbra/Api/Api/MetaApi.cs b/Penumbra/Api/Api/MetaApi.cs index 7c0cd5fc..5cffc811 100644 --- a/Penumbra/Api/Api/MetaApi.cs +++ b/Penumbra/Api/Api/MetaApi.cs @@ -66,6 +66,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); + MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value.Entry))); } return Functions.ToCompressedBase64(array, 0); @@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } WriteCache(zipStream, cache.Atch); + WriteCache(zipStream, cache.Shp); + WriteCache(zipStream, cache.Atr); } } @@ -140,6 +144,86 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } } + public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8); + public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8); + public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P'; + public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8); + public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8); + public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8); + public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P'; + public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H'; + public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8); + public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8); + + private static unsafe string CompressMetaManipulationsV2(ModCollection? collection) + { + using var ms = new MemoryStream(); + ms.Capacity = 1024; + using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true)) + { + zipStream.Write((byte)2); + zipStream.Write("META0002"u8); + if (collection?.MetaCache is { } cache) + { + WriteCache(zipStream, cache.Imc, ImcKey); + WriteCache(zipStream, cache.Eqp, EqpKey); + WriteCache(zipStream, cache.Eqdp, EqdpKey); + WriteCache(zipStream, cache.Est, EstKey); + WriteCache(zipStream, cache.Rsp, RspKey); + WriteCache(zipStream, cache.Gmp, GmpKey); + cache.GlobalEqp.EnterReadLock(); + + try + { + if (cache.GlobalEqp.Count > 0) + { + zipStream.Write(GeqpKey); + zipStream.Write(cache.GlobalEqp.Count); + foreach (var (globalEqp, _) in cache.GlobalEqp) + zipStream.Write(new ReadOnlySpan(&globalEqp, sizeof(GlobalEqpManipulation))); + } + } + finally + { + cache.GlobalEqp.ExitReadLock(); + } + + WriteCache(zipStream, cache.Atch, AtchKey); + WriteCache(zipStream, cache.Shp, ShpKey); + WriteCache(zipStream, cache.Atr, AtrKey); + } + } + + ms.Flush(); + ms.Position = 0; + var data = ms.GetBuffer().AsSpan(0, (int)ms.Length); + return Convert.ToBase64String(data); + + void WriteCache(Stream stream, MetaCacheBase metaCache, uint label) + where TKey : unmanaged, IMetaIdentifier + where TValue : unmanaged + { + metaCache.EnterReadLock(); + try + { + if (metaCache.Count <= 0) + return; + + stream.Write(label); + stream.Write(metaCache.Count); + foreach (var (identifier, (_, value)) in metaCache) + { + stream.Write(identifier); + stream.Write(value); + } + } + finally + { + metaCache.ExitReadLock(); + } + } + } + /// /// Convert manipulations from a transmitted base64 string to actual manipulations. /// The empty string is treated as an empty set. @@ -170,6 +254,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver { case 0: return ConvertManipsV0(data, out manips); case 1: return ConvertManipsV1(data, out manips); + case 2: return ConvertManipsV2(data, out manips); default: Penumbra.Log.Debug($"Invalid version for manipulations: {version}."); manips = null; @@ -185,6 +270,131 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver } } + private static bool ConvertManipsV2(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) + { + if (!data.StartsWith("META0002"u8)) + { + Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix."); + manips = null; + return false; + } + + manips = new MetaDictionary(); + var r = new SpanBinaryReader(data[8..]); + while (r.Remaining > 4) + { + var prefix = r.ReadUInt32(); + var count = r.Remaining > 4 ? r.ReadInt32() : 0; + if (count is 0) + continue; + + switch (prefix) + { + case ImcKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case EqpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case EqdpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case EstKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case RspKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case GmpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case GeqpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier)) + return false; + } + + break; + case AtchKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case ShpKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + case AtrKey: + for (var i = 0; i < count; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + break; + } + } + + return true; + } + private static bool ConvertManipsV1(ReadOnlySpan data, [NotNullWhen(true)] out MetaDictionary? manips) { if (!data.StartsWith("META0001"u8)) @@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver if (!identifier.Validate() || !manips.TryAdd(identifier, value)) return false; } + + // Shp and Atr was added later + if (r.Position < r.Count) + { + var shpCount = r.ReadInt32(); + for (var i = 0; i < shpCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + + var atrCount = r.ReadInt32(); + for (var i = 0; i < atrCount; ++i) + { + var identifier = r.Read(); + var value = r.Read(); + if (!identifier.Validate() || !manips.TryAdd(identifier, value)) + return false; + } + } } return true; From 74bd1cf911fdd906b16c3bce1acc38716bf14efe Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 30 May 2025 14:36:33 +0200 Subject: [PATCH 721/865] Fix checking the flags for all races and genders for specific IDs in shapes/attributes. --- Penumbra/Collections/Cache/ShapeAttributeHashSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index 74691e41..9670928f 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -59,7 +59,7 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) => GenderRaceIndices.TryGetValue(genderRace, out var index) && TryGetValue((slot, id), out var flags) - && (flags & (1ul << index)) is not 0; + && ((flags & 1ul) is not 0 || (flags & (1ul << index)) is not 0); public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) { From 75f4e66dbf5da8984e2cbe825fb8ba8d8a7e0652 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 30 May 2025 12:38:32 +0000 Subject: [PATCH 722/865] [CI] Updating repo.json for 1.4.0.1 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 94020c96..7cdc7bbb 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.0", - "TestingAssemblyVersion": "1.4.0.0", + "AssemblyVersion": "1.4.0.1", + "TestingAssemblyVersion": "1.4.0.1", "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.0/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.0/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.4.0.1/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 b48c4f440acdc02a32c9518023269e1524a8e191 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jun 2025 13:04:01 +0200 Subject: [PATCH 723/865] Make attributes and shapes completely toggleable. --- Penumbra/Collections/Cache/AtrCache.cs | 27 ++- .../Cache/ShapeAttributeHashSet.cs | 171 +++++++++++------- Penumbra/Collections/Cache/ShpCache.cs | 31 +++- Penumbra/Meta/ShapeAttributeManager.cs | 37 ++-- Penumbra/UI/ConfigWindow.cs | 12 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 123 +++++++------ 6 files changed, 245 insertions(+), 156 deletions(-) diff --git a/Penumbra/Collections/Cache/AtrCache.cs b/Penumbra/Collections/Cache/AtrCache.cs index 757ddaa2..b017da32 100644 --- a/Penumbra/Collections/Cache/AtrCache.cs +++ b/Penumbra/Collections/Cache/AtrCache.cs @@ -8,10 +8,12 @@ namespace Penumbra.Collections.Cache; public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace) - => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.Contains(slot, id, genderRace); + => DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.CheckEntry(slot, id, genderRace) is false; + public int EnabledCount { get; private set; } public int DisabledCount { get; private set; } + internal IReadOnlyDictionary Data => _atrData; @@ -21,24 +23,28 @@ public sealed class AtrCache(MetaFileManager manager, ModCollection collection) { Clear(); _atrData.Clear(); + DisabledCount = 0; + EnabledCount = 0; } protected override void Dispose(bool _) - => Clear(); + => Reset(); protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry) { if (!_atrData.TryGetValue(identifier.Attribute, out var value)) { - if (entry.Value) - return; - value = []; _atrData.Add(identifier.Attribute, value); } - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, !entry.Value)) - ++DisabledCount; + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _)) + { + if (entry.Value) + ++EnabledCount; + else + ++DisabledCount; + } } protected override void RevertModInternal(AtrIdentifier identifier) @@ -46,9 +52,12 @@ public sealed class AtrCache(MetaFileManager manager, ModCollection collection) if (!_atrData.TryGetValue(identifier.Attribute, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which)) { - --DisabledCount; + if (which) + --EnabledCount; + else + --DisabledCount; if (value.IsEmpty) _atrData.Remove(identifier.Attribute); } diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index 9670928f..e50ceaa2 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -19,93 +19,126 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI public static readonly FrozenDictionary GenderRaceIndices = GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index); - private readonly BitArray _allIds = new((ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + private readonly BitArray _allIds = new(2 * (ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count); + + public bool? this[HumanSlot slot] + => AllCheck(ToIndex(slot, 0)); + + public bool? this[GenderRace genderRace] + => ToIndex(HumanSlot.Unknown, genderRace, out var index) ? AllCheck(index) : null; + + public bool? this[HumanSlot slot, GenderRace genderRace] + => ToIndex(slot, genderRace, out var index) ? AllCheck(index) : null; + + public bool? All + => Convert(_allIds[2 * AllIndex], _allIds[2 * AllIndex + 1]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private bool CheckGroups(HumanSlot slot, GenderRace genderRace) - { - if (All || this[slot]) - return true; - - if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) - return false; - - if (_allIds[ToIndex(HumanSlot.Unknown, index)]) - return true; - - return _allIds[ToIndex(slot, index)]; - } - - public bool this[HumanSlot slot] - => _allIds[ToIndex(slot, 0)]; - - public bool this[GenderRace genderRace] - => ToIndex(HumanSlot.Unknown, genderRace, out var index) && _allIds[index]; - - public bool this[HumanSlot slot, GenderRace genderRace] - => ToIndex(slot, genderRace, out var index) && _allIds[index]; - - public bool All - => _allIds[AllIndex]; + private bool? AllCheck(int idx) + => Convert(_allIds[idx], _allIds[idx + 1]); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static int ToIndex(HumanSlot slot, int genderRaceIndex) - => slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count; + => 2 * (slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count); - public bool Contains(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => CheckGroups(slot, genderRace) || ContainsEntry(slot, id, genderRace); - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private bool ContainsEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) - => GenderRaceIndices.TryGetValue(genderRace, out var index) - && TryGetValue((slot, id), out var flags) - && ((flags & 1ul) is not 0 || (flags & (1ul << index)) is not 0); - - public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool value) + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public bool? CheckEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace) { + if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) + return null; + + // Check for specific ID. + if (TryGetValue((slot, id), out var flags)) + { + // Check completely specified entry. + if (Convert(flags, 2 * index) is { } specified) + return specified; + + // Check any gender / race. + if (Convert(flags, 0) is { } anyGr) + return anyGr; + } + + // Check for specified gender / race and slot, but no ID. + if (AllCheck(ToIndex(slot, index)) is { } noIdButGr) + return noIdButGr; + + // Check for specified gender / race but no slot or ID. + if (AllCheck(ToIndex(HumanSlot.Unknown, index)) is { } noSlotButGr) + return noSlotButGr; + + // Check for specified slot but no gender / race or ID. + if (AllCheck(ToIndex(slot, 0)) is { } noGrButSlot) + return noGrButSlot; + + return All; + } + + public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool? value, out bool which) + { + which = false; if (!GenderRaceIndices.TryGetValue(genderRace, out var index)) return false; if (!id.HasValue) { var slotIndex = ToIndex(slot, index); - var old = _allIds[slotIndex]; - _allIds[slotIndex] = value; - return old != value; - } - - if (value) - { - if (TryGetValue((slot, id.Value), out var flags)) + var ret = false; + if (value is true) { - var newFlags = flags | (1ul << index); - if (newFlags == flags) - return false; + if (!_allIds[slotIndex]) + ret = true; + _allIds[slotIndex] = true; + _allIds[slotIndex + 1] = false; + } + else if (value is false) + { + if (!_allIds[slotIndex + 1]) + ret = true; + _allIds[slotIndex] = false; + _allIds[slotIndex + 1] = true; + } + else + { + if (_allIds[slotIndex]) + { + which = true; + ret = true; + } + else if (_allIds[slotIndex + 1]) + { + which = false; + ret = true; + } - this[(slot, id.Value)] = newFlags; - return true; + _allIds[slotIndex] = false; + _allIds[slotIndex + 1] = false; } - this[(slot, id.Value)] = 1ul << index; - return true; + return ret; } - else if (TryGetValue((slot, id.Value), out var flags)) + + if (TryGetValue((slot, id.Value), out var flags)) { - var newFlags = flags & ~(1ul << index); + var newFlags = value switch + { + true => (flags | (1ul << index)) & ~(1ul << (index + 1)), + false => (flags & ~(1ul << index)) | (1ul << (index + 1)), + _ => flags & ~(1ul << index) & ~(1ul << (index + 1)), + }; if (newFlags == flags) return false; - if (newFlags is 0) - { - Remove((slot, id.Value)); - return true; - } - this[(slot, id.Value)] = newFlags; + which = (flags & (1ul << index)) is not 0; return true; } - return false; + if (value is null) + return false; + + this[(slot, id.Value)] = 1ul << (index + (value.Value ? 0 : 1)); + return true; } public new void Clear() @@ -128,4 +161,20 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI index = ToIndex(slot, index); return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool? Convert(bool trueValue, bool falseValue) + => trueValue ? true : falseValue ? false : null; + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static bool? Convert(ulong mask, int idx) + { + mask >>= idx; + return (mask & 3) switch + { + 1 => true, + 2 => false, + _ => null, + }; + } } diff --git a/Penumbra/Collections/Cache/ShpCache.cs b/Penumbra/Collections/Cache/ShpCache.cs index 22547d25..d8c3a036 100644 --- a/Penumbra/Collections/Cache/ShpCache.cs +++ b/Penumbra/Collections/Cache/ShpCache.cs @@ -8,7 +8,10 @@ namespace Penumbra.Collections.Cache; public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase(manager, collection) { public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) - => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id, genderRace); + => EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true; + + public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace) + => DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false; internal IReadOnlyDictionary State(ShapeConnectorCondition connector) => connector switch @@ -20,7 +23,8 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) _ => [], }; - public int EnabledCount { get; private set; } + public int EnabledCount { get; private set; } + public int DisabledCount { get; private set; } private readonly Dictionary _shpData = []; private readonly Dictionary _wristConnectors = []; @@ -34,10 +38,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) _wristConnectors.Clear(); _waistConnectors.Clear(); _ankleConnectors.Clear(); + EnabledCount = 0; + DisabledCount = 0; } protected override void Dispose(bool _) - => Clear(); + => Reset(); protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry) { @@ -55,15 +61,17 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) { if (!dict.TryGetValue(identifier.Shape, out var value)) { - if (!entry.Value) - return; - value = []; dict.Add(identifier.Shape, value); } - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value)) - ++EnabledCount; + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _)) + { + if (entry.Value) + ++EnabledCount; + else + ++DisabledCount; + } } } @@ -84,9 +92,12 @@ public sealed class ShpCache(MetaFileManager manager, ModCollection collection) if (!dict.TryGetValue(identifier.Shape, out var value)) return; - if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, false)) + if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which)) { - --EnabledCount; + if (which) + --EnabledCount; + else + --DisabledCount; if (value.IsEmpty) dict.Remove(identifier.Shape); } diff --git a/Penumbra/Meta/ShapeAttributeManager.cs b/Penumbra/Meta/ShapeAttributeManager.cs index e9c9c169..a742806f 100644 --- a/Penumbra/Meta/ShapeAttributeManager.cs +++ b/Penumbra/Meta/ShapeAttributeManager.cs @@ -106,46 +106,59 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable private void UpdateDefaultMasks(Model human, ShpCache cache) { + var genderRace = (GenderRace)human.AsHuman->RaceSexId; foreach (var (shape, topIndex) in _temporaryShapes[1]) { - if (shape.IsWrist() && _temporaryShapes[2].TryGetValue(shape, out var handIndex)) + if (shape.IsWrist() + && _temporaryShapes[2].TryGetValue(shape, out var handIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Hands, _ids[2], genderRace) + && human.AsHuman->Models[1] is not null + && human.AsHuman->Models[2] is not null) { human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Wrists), HumanSlot.Body, HumanSlot.Hands, 1, 2); + CheckCondition(cache.State(ShapeConnectorCondition.Wrists), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2); } - if (shape.IsWaist() && _temporaryShapes[3].TryGetValue(shape, out var legIndex)) + if (shape.IsWaist() + && _temporaryShapes[3].TryGetValue(shape, out var legIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace) + && human.AsHuman->Models[1] is not null + && human.AsHuman->Models[3] is not null) { human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex; human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Waist), HumanSlot.Body, HumanSlot.Legs, 1, 3); + CheckCondition(cache.State(ShapeConnectorCondition.Waist), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3); } } foreach (var (shape, bottomIndex) in _temporaryShapes[3]) { - if (shape.IsAnkle() && _temporaryShapes[4].TryGetValue(shape, out var footIndex)) + if (shape.IsAnkle() + && _temporaryShapes[4].TryGetValue(shape, out var footIndex) + && !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace) + && !cache.ShouldBeDisabled(shape, HumanSlot.Feet, _ids[4], genderRace) + && human.AsHuman->Models[3] is not null + && human.AsHuman->Models[4] is not null) { human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex; human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex; - CheckCondition(cache.State(ShapeConnectorCondition.Ankles), HumanSlot.Legs, HumanSlot.Feet, 3, 4); + CheckCondition(cache.State(ShapeConnectorCondition.Ankles), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4); } } return; - void CheckCondition(IReadOnlyDictionary dict, HumanSlot slot1, + void CheckCondition(IReadOnlyDictionary dict, GenderRace genderRace, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2) { - if (dict.Count is 0) - return; - foreach (var (shape, set) in dict) { - if (set.Contains(slot1, _ids[idx1], GenderRace.Unknown) && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) + if (set.CheckEntry(slot1, _ids[idx1], genderRace) is true && _temporaryShapes[idx1].TryGetValue(shape, out var index1)) human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1; - if (set.Contains(slot2, _ids[idx2], GenderRace.Unknown) && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) + if (set.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2)) human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2; } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 53fa0b33..64d370b5 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -17,12 +17,12 @@ namespace Penumbra.UI; public sealed class ConfigWindow : Window, IUiService { private readonly IDalamudPluginInterface _pluginInterface; - private readonly Configuration _config; - private readonly PerformanceTracker _tracker; - private readonly ValidityChecker _validityChecker; - private Penumbra? _penumbra; - private ConfigTabBar _configTabs = null!; - private string? _lastException; + private readonly Configuration _config; + private readonly PerformanceTracker _tracker; + private readonly ValidityChecker _validityChecker; + private Penumbra? _penumbra; + private ConfigTabBar _configTabs = null!; + private string? _lastException; public ConfigWindow(PerformanceTracker tracker, IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index fd37bf35..2de78c66 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -52,11 +52,14 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) return; ImUtf8.TableSetupColumn("Attribute"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("Disabled"u8, ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); - foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data) - DrawShapeAttribute(attribute, set); + foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data.OrderBy(a => a.Key)) + { + ImUtf8.DrawTableColumn(attribute.AsSpan); + DrawValues(attribute, set); + } } private unsafe void DrawCollectionShapeCache(Actor actor) @@ -72,83 +75,87 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Condition"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shape"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); - ImUtf8.TableSetupColumn("Enabled"u8, ImGuiTableColumnFlags.WidthStretch); + ImUtf8.TableSetupColumn("State"u8, ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); foreach (var condition in Enum.GetValues()) { - foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition)) + foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition).OrderBy(shp => shp.Key)) { ImUtf8.DrawTableColumn(condition.ToString()); - DrawShapeAttribute(shape, set); + ImUtf8.DrawTableColumn(shape.AsSpan); + DrawValues(shape, set); } } } - private static void DrawShapeAttribute(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) + private static void DrawValues(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) { - ImUtf8.DrawTableColumn(shapeAttribute.AsSpan); - if (set.All) - { - ImUtf8.DrawTableColumn("All"u8); - } - else - { - ImGui.TableNextColumn(); - foreach (var slot in ShapeAttributeManager.UsedModels) - { - if (!set[slot]) - continue; + ImGui.TableNextColumn(); - ImUtf8.Text($"All {slot.ToName()}, "); + if (set.All is { } value) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value); + ImUtf8.Text("All"u8); + ImGui.SameLine(0, 0); + } + + foreach (var slot in ShapeAttributeManager.UsedModels) + { + if (set[slot] is not { } value2) + continue; + + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value2); + ImUtf8.Text($"All {slot.ToName()}, "); + ImGui.SameLine(0, 0); + } + + foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + { + if (set[gr] is { } value3) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value3); + ImUtf8.Text($"All {gr.ToName()}, "); ImGui.SameLine(0, 0); } - - foreach (var gr in ShapeAttributeHashSet.GenderRaceValues.Skip(1)) + else { - if (set[gr]) + foreach (var slot in ShapeAttributeManager.UsedModels) { - ImUtf8.Text($"All {gr.ToName()}, "); - ImGui.SameLine(0, 0); - } - else - { - foreach (var slot in ShapeAttributeManager.UsedModels) - { - if (!set[slot, gr]) - continue; - - ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); - ImGui.SameLine(0, 0); - } - } - } - - - foreach (var ((slot, id), flags) in set) - { - if ((flags & 1ul) is not 0) - { - if (set[slot]) + if (set[slot, gr] is not { } value4) continue; - ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value4); + ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); ImGui.SameLine(0, 0); } - else - { - var currentFlags = flags >> 1; - var currentIndex = BitOperations.TrailingZeroCount(currentFlags); - while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) - { - var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; - if (set[slot, gr]) - continue; + } + } - ImUtf8.Text($"{gr.ToName()} {slot.ToName()} {id.Id:D4}, "); - currentFlags >>= currentIndex; - currentIndex = BitOperations.TrailingZeroCount(currentFlags); + foreach (var ((slot, id), flags) in set) + { + if ((flags & 3) is not 0) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), (flags & 2) is not 0); + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } + else + { + var currentFlags = flags >> 2; + var currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) + { + var value5 = (currentFlags & 1) is 1; + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr] != value5) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value5); + ImUtf8.Text($"{gr.ToName()} {slot.ToName()} #{id.Id:D4}, "); } + + currentFlags >>= currentIndex * 2; + currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; } } } From 6cba63ac9807b4797166c02b903b872b11890db8 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 1 Jun 2025 13:04:20 +0200 Subject: [PATCH 724/865] Make shape names editable in models. --- .../UI/AdvancedWindow/ModEditWindow.Models.cs | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 0c8c496f..cc592296 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -31,19 +31,18 @@ public partial class ModEditWindow private class LoadedData { - public MdlFile LastFile = null!; + public MdlFile LastFile = null!; public readonly List SubMeshAttributeTags = []; public long[] LodTriCount = []; } - private string _modelNewMaterial = string.Empty; + private string _modelNewMaterial = string.Empty; private readonly LoadedData _main = new(); private readonly LoadedData _preview = new(); - private string _customPath = string.Empty; - private Utf8GamePath _customGamePath = Utf8GamePath.Empty; - + private string _customPath = string.Empty; + private Utf8GamePath _customGamePath = Utf8GamePath.Empty; private LoadedData UpdateFile(MdlFile file, bool force, bool disabled) @@ -68,7 +67,7 @@ public partial class ModEditWindow private bool DrawModelPanel(MdlTab tab, bool disabled) { - var ret = tab.Dirty; + var ret = tab.Dirty; var data = UpdateFile(tab.Mdl, ret, disabled); DrawVersionUpdate(tab, disabled); DrawImportExport(tab, disabled); @@ -89,7 +88,8 @@ public partial class ModEditWindow if (disabled || tab.Mdl.Version is not MdlFile.V5) return; - if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, + if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, + "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) return; @@ -350,7 +350,7 @@ public partial class ModEditWindow if (!disabled) { ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("help", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("help", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); } var inputFlags = disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; @@ -369,10 +369,11 @@ public partial class ModEditWindow ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, string.Empty, !validName, true)) { - ret |= true; - tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); - _modelNewMaterial = string.Empty; + ret |= true; + tab.Mdl.Materials = materials.AddItem(_modelNewMaterial); + _modelNewMaterial = string.Empty; } + ImGui.TableNextColumn(); if (!validName && _modelNewMaterial.Length > 0) DrawInvalidMaterialMarker(); @@ -423,14 +424,16 @@ public partial class ModEditWindow // Add markers to invalid materials. if (!tab.ValidateMaterial(temp)) DrawInvalidMaterialMarker(); - + return ret; } private static void DrawInvalidMaterialMarker() { using (ImRaii.PushFont(UiBuilder.IconFont)) + { ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); + } ImGuiUtil.HoverTooltip( "Materials must be either relative (e.g. \"/filename.mtrl\")\n" @@ -498,11 +501,11 @@ public partial class ModEditWindow using var node = ImRaii.TreeNode($"Click to expand"); if (!node) return; - + var flags = ImGuiTableFlags.SizingFixedFit - | ImGuiTableFlags.RowBg - | ImGuiTableFlags.Borders - | ImGuiTableFlags.NoHostExtendX; + | ImGuiTableFlags.RowBg + | ImGuiTableFlags.Borders + | ImGuiTableFlags.NoHostExtendX; using var table = ImRaii.Table(string.Empty, 4, flags); if (!table) return; @@ -590,6 +593,7 @@ public partial class ModEditWindow if (!header) return false; + var ret = false; using (var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit)) { if (table) @@ -650,22 +654,49 @@ public partial class ModEditWindow using (var attributes = ImRaii.TreeNode("Attributes", ImGuiTreeNodeFlags.DefaultOpen)) { if (attributes) - foreach (var attribute in data.LastFile.Attributes) - ImRaii.TreeNode(attribute, ImGuiTreeNodeFlags.Leaf).Dispose(); + for (var i = 0; i < data.LastFile.Attributes.Length; ++i) + { + using var id = ImUtf8.PushId(i); + ref var attribute = ref data.LastFile.Attributes[i]; + var name = attribute; + if (ImUtf8.InputText("##attribute"u8, ref name, "Attribute Name..."u8) && name.Length > 0 && name != attribute) + { + attribute = name; + ret = true; + } + } } using (var bones = ImRaii.TreeNode("Bones", ImGuiTreeNodeFlags.DefaultOpen)) { if (bones) - foreach (var bone in data.LastFile.Bones) - ImRaii.TreeNode(bone, ImGuiTreeNodeFlags.Leaf).Dispose(); + for (var i = 0; i < data.LastFile.Bones.Length; ++i) + { + using var id = ImUtf8.PushId(i); + ref var bone = ref data.LastFile.Bones[i]; + var name = bone; + if (ImUtf8.InputText("##bone"u8, ref name, "Bone Name..."u8) && name.Length > 0 && name != bone) + { + bone = name; + ret = true; + } + } } using (var shapes = ImRaii.TreeNode("Shapes", ImGuiTreeNodeFlags.DefaultOpen)) { if (shapes) - foreach (var shape in data.LastFile.Shapes) - ImRaii.TreeNode(shape.ShapeName, ImGuiTreeNodeFlags.Leaf).Dispose(); + for (var i = 0; i < data.LastFile.Shapes.Length; ++i) + { + using var id = ImUtf8.PushId(i); + ref var shape = ref data.LastFile.Shapes[i]; + var name = shape.ShapeName; + if (ImUtf8.InputText("##shape"u8, ref name, "Shape Name..."u8) && name.Length > 0 && name != shape.ShapeName) + { + shape.ShapeName = name; + ret = true; + } + } } if (data.LastFile.RemainingData.Length > 0) @@ -675,7 +706,7 @@ public partial class ModEditWindow Widget.DrawHexViewer(data.LastFile.RemainingData); } - return false; + return ret; } private static bool GetFirstModel(IEnumerable files, [NotNullWhen(true)] out string? file) From 98203e4e8a6a7bd660d0df4dabebc6c48776450d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 1 Jun 2025 11:06:37 +0000 Subject: [PATCH 725/865] [CI] Updating repo.json for testing_1.4.0.2 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index 7cdc7bbb..e36176d5 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.1", + "TestingAssemblyVersion": "1.4.0.2", "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/1.4.0.1/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.2/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 318a41fe52ad00ce120d08b2c812e11a6a9b014a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 Jun 2025 18:39:46 +0200 Subject: [PATCH 726/865] Add checking for supported features with the currently new supported features 'Atch', 'Shp' and 'Atr'. --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/PluginStateApi.cs | 33 ++++--- Penumbra/Api/IpcProviders.cs | 2 + .../Api/IpcTester/PluginStateIpcTester.cs | 15 ++- Penumbra/Mods/FeatureChecker.cs | 95 +++++++++++++++++++ Penumbra/Mods/Manager/ModDataEditor.cs | 11 +++ Penumbra/Mods/Manager/ModManager.cs | 15 ++- Penumbra/Mods/Mod.cs | 29 +++++- Penumbra/Mods/ModCreator.cs | 11 ++- Penumbra/Mods/ModMeta.cs | 23 ++++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 5 + Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 2 +- schemas/mod_meta-v3.json | 8 ++ 14 files changed, 216 insertions(+), 37 deletions(-) create mode 100644 Penumbra/Mods/FeatureChecker.cs diff --git a/Penumbra.Api b/Penumbra.Api index 14652039..ff7b3b40 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1465203967d08519c6716292bc5e5094c7fbcacc +Subproject commit ff7b3b4014a97455f823380c78b8a7c5107f8e2f diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index 38125627..7ca41324 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 = 9; + public const int FeatureVersion = 10; public void Dispose() { diff --git a/Penumbra/Api/Api/PluginStateApi.cs b/Penumbra/Api/Api/PluginStateApi.cs index d69df448..f74553d1 100644 --- a/Penumbra/Api/Api/PluginStateApi.cs +++ b/Penumbra/Api/Api/PluginStateApi.cs @@ -1,39 +1,38 @@ +using System.Collections.Frozen; using Newtonsoft.Json; using OtterGui.Services; using Penumbra.Communication; +using Penumbra.Mods; using Penumbra.Services; namespace Penumbra.Api.Api; -public class PluginStateApi : IPenumbraApiPluginState, IApiService +public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - - public PluginStateApi(Configuration config, CommunicatorService communicator) - { - _config = config; - _communicator = communicator; - } - public string GetModDirectory() - => _config.ModDirectory; + => config.ModDirectory; public string GetConfiguration() - => JsonConvert.SerializeObject(_config, Formatting.Indented); + => JsonConvert.SerializeObject(config, Formatting.Indented); public event Action? ModDirectoryChanged { - add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); - remove => _communicator.ModDirectoryChanged.Unsubscribe(value!); + add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api); + remove => communicator.ModDirectoryChanged.Unsubscribe(value!); } public bool GetEnabledState() - => _config.EnableMods; + => config.EnableMods; public event Action? EnabledChange { - add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); - remove => _communicator.EnabledChanged.Unsubscribe(value!); + add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api); + remove => communicator.EnabledChanged.Unsubscribe(value!); } + + public FrozenSet SupportedFeatures + => FeatureChecker.SupportedFeatures.ToFrozenSet(); + + public string[] CheckSupportedFeatures(IEnumerable requiredFeatures) + => requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray(); } diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index f5a6c16d..7dcee375 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -80,6 +80,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState), IpcSubscribers.EnabledChange.Provider(pi, api.PluginState), + IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState), + IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState), IpcSubscribers.RedrawObject.Provider(pi, api.Redraw), IpcSubscribers.RedrawAll.Provider(pi, api.Redraw), diff --git a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs index df82033d..a1bf4fc4 100644 --- a/Penumbra/Api/IpcTester/PluginStateIpcTester.cs +++ b/Penumbra/Api/IpcTester/PluginStateIpcTester.cs @@ -5,6 +5,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; @@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester; public class PluginStateIpcTester : IUiService, IDisposable { - private readonly IDalamudPluginInterface _pi; + private readonly IDalamudPluginInterface _pi; public readonly EventSubscriber ModDirectoryChanged; public readonly EventSubscriber Initialized; public readonly EventSubscriber Disposed; @@ -26,6 +27,9 @@ public class PluginStateIpcTester : IUiService, IDisposable private readonly List _initializedList = []; private readonly List _disposedList = []; + private string _requiredFeatureString = string.Empty; + private string[] _requiredFeatures = []; + private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch; private bool? _lastEnabledValue; @@ -48,12 +52,15 @@ public class PluginStateIpcTester : IUiService, IDisposable EnabledChange.Dispose(); } + public void Draw() { using var _ = ImRaii.TreeNode("Plugin State"); if (!_) return; + if (ImUtf8.InputText("Required Features"u8, ref _requiredFeatureString)) + _requiredFeatures = _requiredFeatureString.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit); if (!table) return; @@ -71,6 +78,12 @@ public class PluginStateIpcTester : IUiService, IDisposable IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change"); ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never"); + IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features"); + ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke())); + + IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features"); + ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures))); + DrawConfigPopup(); IpcTester.DrawIntro(GetConfiguration.Label, "Configuration"); if (ImGui.Button("Get")) diff --git a/Penumbra/Mods/FeatureChecker.cs b/Penumbra/Mods/FeatureChecker.cs new file mode 100644 index 00000000..5800ef07 --- /dev/null +++ b/Penumbra/Mods/FeatureChecker.cs @@ -0,0 +1,95 @@ +using System.Collections.Frozen; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; +using OtterGui.Text; +using Penumbra.Mods.Manager; +using Penumbra.UI.Classes; +using Notification = OtterGui.Classes.Notification; + +namespace Penumbra.Mods; + +public static class FeatureChecker +{ + /// Manually setup supported features to exclude None and Invalid and not make something supported too early. + private static readonly FrozenDictionary SupportedFlags = new[] + { + FeatureFlags.Atch, + FeatureFlags.Shp, + FeatureFlags.Atr, + }.ToFrozenDictionary(f => f.ToString(), f => f); + + public static IReadOnlyCollection SupportedFeatures + => SupportedFlags.Keys; + + public static FeatureFlags ParseFlags(string modDirectory, string modName, IEnumerable features) + { + var featureFlags = FeatureFlags.None; + HashSet missingFeatures = []; + foreach (var feature in features) + { + if (SupportedFlags.TryGetValue(feature, out var featureFlag)) + featureFlags |= featureFlag; + else + missingFeatures.Add(feature); + } + + if (missingFeatures.Count > 0) + { + Penumbra.Messager.AddMessage(new Notification( + $"Please update Penumbra to use the mod {modName}{(modDirectory != modName ? $" at {modDirectory}" : string.Empty)}!\n\nLoading failed because it requires the unsupported feature{(missingFeatures.Count > 1 ? $"s\n\n\t[{string.Join("], [", missingFeatures)}]." : $" [{missingFeatures.First()}].")}", + NotificationType.Warning)); + return FeatureFlags.Invalid; + } + + return featureFlags; + } + + public static bool Supported(string features) + => SupportedFlags.ContainsKey(features); + + public static void DrawFeatureFlagInput(ModDataEditor editor, Mod mod, float width) + { + const int numButtons = 5; + var innerSpacing = ImGui.GetStyle().ItemInnerSpacing; + var size = new Vector2((width - (numButtons - 1) * innerSpacing.X) / numButtons, 0); + var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + var textColor = ImGui.GetColorU32(ImGuiCol.TextDisabled); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, innerSpacing) + .Push(ImGuiStyleVar.FrameBorderSize, 0); + using (var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()) + .Push(ImGuiCol.Button, buttonColor) + .Push(ImGuiCol.Text, textColor)) + { + foreach (var flag in SupportedFlags.Values) + { + if (mod.RequiredFeatures.HasFlag(flag)) + { + style.Push(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale); + color.Pop(2); + if (ImUtf8.Button($"{flag}", size)) + editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures & ~flag); + color.Push(ImGuiCol.Button, buttonColor) + .Push(ImGuiCol.Text, textColor); + style.Pop(); + } + else if (ImUtf8.Button($"{flag}", size)) + { + editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures | flag); + } + + ImGui.SameLine(); + } + } + + if (ImUtf8.ButtonEx("Compute"u8, "Compute the required features automatically from the used features."u8, size)) + editor.ChangeRequiredFeatures(mod, mod.ComputeRequiredFeatures()); + + ImGui.SameLine(); + if (ImUtf8.ButtonEx("Clear"u8, "Clear all required features."u8, size)) + editor.ChangeRequiredFeatures(mod, FeatureFlags.None); + + ImGui.SameLine(); + ImUtf8.Text("Required Features"u8); + } +} diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 1349b525..fc4fdadc 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -26,6 +26,7 @@ public enum ModDataChangeType : ushort Image = 0x1000, DefaultChangedItems = 0x2000, PreferredChangedItems = 0x4000, + RequiredFeatures = 0x8000, } public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService @@ -95,6 +96,16 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic mod.Website = newWebsite; saveService.QueueSave(new ModMeta(mod)); communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); + } + + public void ChangeRequiredFeatures(Mod mod, FeatureFlags flags) + { + if (mod.RequiredFeatures == flags) + return; + + mod.RequiredFeatures = flags; + saveService.QueueSave(new ModMeta(mod)); + communicatorService.ModDataChanged.Invoke(ModDataChangeType.RequiredFeatures, mod, null); } public void ChangeModTag(Mod mod, int tagIdx, string newTag) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index bf1b6637..32dac049 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -143,9 +143,10 @@ public sealed class ModManager : ModStorage, IDisposable, IService _communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); if (!Creator.ReloadMod(mod, true, false, out var metaChange)) { - Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); + if (mod.RequiredFeatures is not FeatureFlags.Invalid) + Penumbra.Log.Warning(mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead."); RemoveMod(mod); return; } @@ -251,12 +252,8 @@ public sealed class ModManager : ModStorage, IDisposable, IService { switch (type) { - case ModPathChangeType.Added: - SetNew(mod); - break; - case ModPathChangeType.Deleted: - SetKnown(mod); - break; + case ModPathChangeType.Added: SetNew(mod); break; + case ModPathChangeType.Deleted: SetKnown(mod); break; case ModPathChangeType.Moved: if (oldDirectory != null && newDirectory != null) DataEditor.MoveDataFile(oldDirectory, newDirectory); diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 99f86517..e262e8f1 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -1,4 +1,3 @@ -using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; using Penumbra.GameData.Data; @@ -12,6 +11,16 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; +[Flags] +public enum FeatureFlags : ulong +{ + None = 0, + Atch = 1ul << 0, + Shp = 1ul << 1, + Atr = 1ul << 2, + Invalid = 1ul << 62, +} + public sealed class Mod : IMod { public static readonly TemporaryMod ForcedFiles = new() @@ -57,6 +66,7 @@ public sealed class Mod : IMod public string Image { get; internal set; } = string.Empty; public IReadOnlyList ModTags { get; internal set; } = []; public HashSet DefaultPreferredItems { get; internal set; } = []; + public FeatureFlags RequiredFeatures { get; internal set; } = 0; // Local Data @@ -70,6 +80,23 @@ public sealed class Mod : IMod public readonly DefaultSubMod Default; public readonly List Groups = []; + /// Compute the required feature flags for this mod. + public FeatureFlags ComputeRequiredFeatures() + { + var flags = FeatureFlags.None; + foreach (var option in AllDataContainers) + { + if (option.Manipulations.Atch.Count > 0) + flags |= FeatureFlags.Atch; + if (option.Manipulations.Atr.Count > 0) + flags |= FeatureFlags.Atr; + if (option.Manipulations.Shp.Count > 0) + flags |= FeatureFlags.Shp; + } + + return flags; + } + public AppliedModData GetData(ModSettings? settings = null) { if (settings is not { Enabled: true }) diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index df476a6f..1bb2a073 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -28,7 +28,8 @@ public partial class ModCreator( MetaFileManager metaFileManager, GamePathParser gamePathParser) : IService { - public readonly Configuration Config = config; + public const FeatureFlags SupportedFeatures = FeatureFlags.Atch | FeatureFlags.Shp | FeatureFlags.Atr; + 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) @@ -74,7 +75,7 @@ public partial class ModCreator( return false; modDataChange = ModMeta.Load(dataEditor, this, mod); - if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0) + if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0 || mod.RequiredFeatures is FeatureFlags.Invalid) return false; modDataChange |= ModLocalData.Load(dataEditor, mod); @@ -82,9 +83,9 @@ public partial class ModCreator( LoadAllGroups(mod); if (incorporateMetaChanges) IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges); - else if (deleteDefaultMetaChanges) - ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); - + else if (deleteDefaultMetaChanges) + ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false); + return true; } diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 1b104af4..b52eecf4 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.Structs; -using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; @@ -28,6 +27,19 @@ public readonly struct ModMeta(Mod mod) : ISavable { nameof(Mod.ModTags), JToken.FromObject(mod.ModTags) }, { nameof(Mod.DefaultPreferredItems), JToken.FromObject(mod.DefaultPreferredItems) }, }; + if (mod.RequiredFeatures is not FeatureFlags.None) + { + var features = mod.RequiredFeatures; + var array = new JArray(); + foreach (var flag in Enum.GetValues()) + { + if ((features & flag) is not FeatureFlags.None) + array.Add(flag.ToString()); + } + + jObject[nameof(Mod.RequiredFeatures)] = array; + } + using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; jObject.WriteTo(jWriter); @@ -60,6 +72,8 @@ public readonly struct ModMeta(Mod mod) : ISavable var modTags = (json[nameof(Mod.ModTags)] as JArray)?.Values().OfType(); var defaultItems = (json[nameof(Mod.DefaultPreferredItems)] as JArray)?.Values().Select(i => (CustomItemId)i).ToHashSet() ?? []; + var requiredFeatureArray = (json[nameof(Mod.RequiredFeatures)] as JArray)?.Values() ?? []; + var requiredFeatures = FeatureChecker.ParseFlags(mod.ModPath.Name, newName.Length > 0 ? newName : mod.Name.Length > 0 ? mod.Name : "Unknown", requiredFeatureArray!); ModDataChangeType changes = 0; if (mod.Name != newName) @@ -111,6 +125,13 @@ public readonly struct ModMeta(Mod mod) : ISavable editor.SaveService.ImmediateSave(new ModMeta(mod)); } + // Required features get checked during parsing, in which case the new required features signal invalid. + if (requiredFeatures != mod.RequiredFeatures) + { + changes |= ModDataChangeType.RequiredFeatures; + mod.RequiredFeatures = requiredFeatures; + } + changes |= ModLocalData.UpdateTags(mod, modTags, null); return changes; diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 1e6afa09..478ab892 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -64,6 +64,10 @@ public class ModPanelEditTab( messager.NotificationMessage(e.Message, NotificationType.Warning, false); } + UiHelpers.DefaultLineSpace(); + + FeatureChecker.DrawFeatureFlagInput(modManager.DataEditor, _mod, UiHelpers.InputTextWidth.X); + UiHelpers.DefaultLineSpace(); var sharedTagsEnabled = predefinedTagManager.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; @@ -76,6 +80,7 @@ public class ModPanelEditTab( predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false, selector.Selected!); + UiHelpers.DefaultLineSpace(); addGroupDrawer.Draw(_mod, UiHelpers.InputTextWidth.X); UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index 2de78c66..a7bfd49c 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -96,7 +96,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (set.All is { } value) { using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value); - ImUtf8.Text("All"u8); + ImUtf8.Text("All, "u8); ImGui.SameLine(0, 0); } diff --git a/schemas/mod_meta-v3.json b/schemas/mod_meta-v3.json index ed63a228..6fc68714 100644 --- a/schemas/mod_meta-v3.json +++ b/schemas/mod_meta-v3.json @@ -52,6 +52,14 @@ "type": "integer" }, "uniqueItems": true + }, + "RequiredFeatures": { + "description": "A list of required features by name.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true } }, "required": [ From 535694e9c8fd5377abcfbf32b5149d1111d4f18a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 7 Jun 2025 22:10:17 +0200 Subject: [PATCH 727/865] Update some BNPC Names. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 14b3641f..94076bf6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 14b3641f0fb520cb829ceb50fa7cb31255a1da4e +Subproject commit 94076bf6bba27c02e0adbafa1c5cc9c279a0b5df From 4c0e6d2a67d5964717c9057cbcec7c73eb9651bf Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 7 Jun 2025 22:10:59 +0200 Subject: [PATCH 728/865] Update Mod Merger for other group types. --- Penumbra/Mods/Editor/ModMerger.cs | 139 +++++++++++++++--- .../Manager/OptionEditor/ModOptionEditor.cs | 48 +++--- 2 files changed, 139 insertions(+), 48 deletions(-) diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 88941edf..bb84173a 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -6,6 +6,7 @@ using OtterGui.Extensions; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Communication; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Mods.SubMods; @@ -44,13 +45,13 @@ public class ModMerger : IDisposable, IService public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { - _editor = editor; - _selection = selection; - _duplicates = duplicates; - _communicator = communicator; - _creator = creator; - _config = config; - _mods = mods; + _editor = editor; + _selection = selection; + _duplicates = duplicates; + _communicator = communicator; + _creator = creator; + _config = config; + _mods = mods; _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger); } @@ -99,26 +100,117 @@ public class ModMerger : IDisposable, IService foreach (var originalGroup in MergeFromMod!.Groups) { - var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); - if (groupCreated) - _createdGroups.Add(groupIdx); - if (group == null) - throw new Exception( - $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); - - foreach (var originalOption in originalGroup.DataContainers) + switch (originalGroup.Type) { - var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); - if (optionCreated) + case GroupType.Single: + case GroupType.Multi: { - _createdOptions.Add(option!); - // #TODO DataContainer <> Option. - MergeIntoOption([originalOption], (IModDataContainer)option!, false); + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); + if (group is null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); + + if (groupCreated) + { + _createdGroups.Add(groupIdx); + group.Description = originalGroup.Description; + group.Image = originalGroup.Image; + group.DefaultSettings = originalGroup.DefaultSettings; + group.Page = originalGroup.Page; + group.Priority = originalGroup.Priority; + } + + foreach (var originalOption in originalGroup.Options) + { + var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.Name); + if (optionCreated) + { + _createdOptions.Add(option!); + MergeIntoOption([(IModDataContainer)originalOption], (IModDataContainer)option!, false); + option!.Description = originalOption.Description; + if (option is MultiSubMod multiOption) + multiOption.Priority = ((MultiSubMod)originalOption).Priority; + } + else + { + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); + } + } + + break; } - else + + case GroupType.Imc when originalGroup is ImcModGroup imc: { - throw new Exception( - $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); + var group = _editor.ImcEditor.AddModGroup(MergeToMod!, imc.Name, imc.Identifier, imc.DefaultEntry); + if (group is null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged."); + + group.AllVariants = imc.AllVariants; + group.OnlyAttributes = imc.OnlyAttributes; + group.Description = imc.Description; + group.Image = imc.Image; + group.DefaultSettings = imc.DefaultSettings; + group.Page = imc.Page; + group.Priority = imc.Priority; + foreach (var originalOption in imc.OptionData) + { + if (originalOption.IsDisableSubMod) + { + _editor.ImcEditor.ChangeCanBeDisabled(group, true); + var disable = group.OptionData.First(s => s.IsDisableSubMod); + disable.Description = originalOption.Description; + disable.Name = originalOption.Name; + continue; + } + + var newOption = _editor.ImcEditor.AddOption(group, originalOption.Name); + if (newOption is null) + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating IMC option {originalOption.FullName}."); + + newOption.Description = originalOption.Description; + newOption.AttributeMask = originalOption.AttributeMask; + } + + break; + } + case GroupType.Combining when originalGroup is CombiningModGroup combining: + { + var group = _editor.CombiningEditor.AddModGroup(MergeToMod!, combining.Name); + if (group is null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged."); + + group.Description = combining.Description; + group.Image = combining.Image; + group.DefaultSettings = combining.DefaultSettings; + group.Page = combining.Page; + group.Priority = combining.Priority; + foreach (var originalOption in combining.OptionData) + { + var option = _editor.CombiningEditor.AddOption(group, originalOption.Name); + if (option is null) + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating combining option {originalOption.FullName}."); + + option.Description = originalOption.Description; + } + + if (group.Data.Count != combining.Data.Count) + throw new Exception( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error caused data container counts in combining group {originalGroup.Name} to differ."); + + foreach (var (originalContainer, container) in combining.Data.Zip(group.Data)) + { + container.Name = originalContainer.Name; + MergeIntoOption([originalContainer], container, false); + } + + + break; } } } @@ -151,7 +243,6 @@ public class ModMerger : IDisposable, IService if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - // #TODO DataContainer <> Option. MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true); } diff --git a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs index 5c5ed4f1..d9d672e3 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ModOptionEditor.cs @@ -15,8 +15,8 @@ public abstract class ModOptionEditor( where TOption : class, IModOption { protected readonly CommunicatorService Communicator = communicator; - protected readonly SaveService SaveService = saveService; - protected readonly Configuration Config = config; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) @@ -25,7 +25,7 @@ public abstract class ModOptionEditor( return null; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; - var group = CreateGroup(mod, newName, maxPriority); + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); @@ -92,8 +92,8 @@ public abstract class ModOptionEditor( /// Delete the given option from the given group. public void DeleteOption(TOption option) { - var mod = option.Mod; - var group = option.Group; + var mod = option.Mod; + var group = option.Group; var optionIdx = option.GetIndex(); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); RemoveOption((TGroup)group, optionIdx); @@ -104,7 +104,7 @@ public abstract class ModOptionEditor( /// Move an option inside the given option group. public void MoveOption(TOption option, int optionIdxTo) { - var idx = option.GetIndex(); + var idx = option.GetIndex(); var group = (TGroup)option.Group; if (!MoveOption(group, idx, optionIdxTo)) return; @@ -113,10 +113,10 @@ public abstract class ModOptionEditor( Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); protected abstract TOption? CloneOption(TGroup group, IModOption option); - protected abstract void RemoveOption(TGroup group, int optionIndex); - protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension @@ -132,22 +132,22 @@ public static class ModOptionChangeTypeExtension { (requiresSaving, requiresReloading, wasPrepared) = type switch { - ModOptionChangeType.GroupRenamed => (true, false, false), - ModOptionChangeType.GroupAdded => (true, false, false), - ModOptionChangeType.GroupDeleted => (true, true, false), - ModOptionChangeType.GroupMoved => (true, false, false), - ModOptionChangeType.GroupTypeChanged => (true, true, true), - ModOptionChangeType.PriorityChanged => (true, true, true), - ModOptionChangeType.OptionAdded => (true, true, true), - ModOptionChangeType.OptionDeleted => (true, true, false), - ModOptionChangeType.OptionMoved => (true, false, false), - ModOptionChangeType.OptionFilesChanged => (false, true, false), - ModOptionChangeType.OptionFilesAdded => (false, true, true), - ModOptionChangeType.OptionSwapsChanged => (false, true, false), - ModOptionChangeType.OptionMetaChanged => (false, true, false), - ModOptionChangeType.DisplayChange => (false, false, false), + ModOptionChangeType.GroupRenamed => (true, false, false), + ModOptionChangeType.GroupAdded => (true, false, false), + ModOptionChangeType.GroupDeleted => (true, true, false), + ModOptionChangeType.GroupMoved => (true, false, false), + ModOptionChangeType.GroupTypeChanged => (true, true, true), + ModOptionChangeType.PriorityChanged => (true, true, true), + ModOptionChangeType.OptionAdded => (true, true, true), + ModOptionChangeType.OptionDeleted => (true, true, false), + ModOptionChangeType.OptionMoved => (true, false, false), + ModOptionChangeType.OptionFilesChanged => (false, true, false), + ModOptionChangeType.OptionFilesAdded => (false, true, true), + ModOptionChangeType.OptionSwapsChanged => (false, true, false), + ModOptionChangeType.OptionMetaChanged => (false, true, false), + ModOptionChangeType.DisplayChange => (false, false, false), ModOptionChangeType.DefaultOptionChanged => (true, false, false), - _ => (false, false, false), + _ => (false, false, false), }; } } From a16fd85a7ea1fb6d6190b5c264f70c78a2b13c63 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jun 2025 11:28:12 +0200 Subject: [PATCH 729/865] Handle .tex files with broken mip map offsets on import, also remove unnecessary mipmaps (any after reaching minimum size once). --- Penumbra/Import/TexToolsImporter.Archives.cs | 4 ++ Penumbra/Import/TexToolsImporter.ModPack.cs | 1 + Penumbra/Import/Textures/TexFileParser.cs | 69 ++++++++++++++++++++ Penumbra/Mods/Manager/ModImportManager.cs | 1 - Penumbra/Services/MigrationManager.cs | 43 ++++++++++++ 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 8166dea7..a80730bf 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Services; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; @@ -146,6 +147,9 @@ public partial class TexToolsImporter case ".mtrl": _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); break; + case ".tex": + _migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions); + break; default: reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); break; diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index 1c28aef2..fd9e50c0 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -259,6 +259,7 @@ public partial class TexToolsImporter { ".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data), ".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data), + ".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data), _ => data.Data, }; diff --git a/Penumbra/Import/Textures/TexFileParser.cs b/Penumbra/Import/Textures/TexFileParser.cs index 220095c1..6a12a0dd 100644 --- a/Penumbra/Import/Textures/TexFileParser.cs +++ b/Penumbra/Import/Textures/TexFileParser.cs @@ -61,6 +61,75 @@ public static class TexFileParser return 13; } + public static unsafe void FixMipOffsets(long size, ref TexFile.TexHeader header, out long newSize) + { + var width = (uint)header.Width; + var height = (uint)header.Height; + var format = header.Format.ToDXGI(); + var bits = format.BitsPerPixel(); + var totalSize = 80u; + size -= totalSize; + var minSize = format.IsCompressed() ? 4u : 1u; + for (var i = 0; i < 13; ++i) + { + var requiredSize = (uint)((long)width * height * bits / 8); + if (requiredSize > size) + { + newSize = totalSize; + if (header.MipCount != i) + { + Penumbra.Log.Debug( + $"-- Mip Map Count in TEX header was {header.MipCount}, but file only contains data for {i} Mip Maps, fixed."); + FixLodOffsets(ref header, i); + } + + return; + } + + if (header.OffsetToSurface[i] != totalSize) + { + Penumbra.Log.Debug( + $"-- Mip Map Offset {i + 1} in TEX header was {header.OffsetToSurface[i]} but should be {totalSize}, fixed."); + header.OffsetToSurface[i] = totalSize; + } + + if (width == minSize && height == minSize) + { + newSize = totalSize; + if (header.MipCount != i) + { + Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints."); + FixLodOffsets(ref header, i); + } + + return; + } + + totalSize += requiredSize; + size -= requiredSize; + width = Math.Max(width / 2, minSize); + height = Math.Max(height / 2, minSize); + } + + newSize = totalSize; + if (header.MipCount != 13) + { + Penumbra.Log.Debug($"-- Mip Map Count in TEX header was {header.MipCount}, but maximum is 13, fixed."); + FixLodOffsets(ref header, 13); + } + + void FixLodOffsets(ref TexFile.TexHeader header, int index) + { + header.MipCount = index; + if (header.LodOffset[2] >= header.MipCount) + header.LodOffset[2] = (byte)(header.MipCount - 1); + if (header.LodOffset[1] >= header.MipCount) + header.LodOffset[1] = header.MipCount > 2 ? (byte)(header.MipCount - 2) : (byte)(header.MipCount - 1); + for (++index; index < 13; ++index) + header.OffsetToSurface[index] = 0; + } + } + private static unsafe void CopyData(ScratchImage image, BinaryReader r) { fixed (byte* ptr = image.Pixels) diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 22cc0c86..bb282262 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -70,7 +70,6 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd _import = null; } - public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod) { if (!_modsToAdd.TryDequeue(out var directory)) diff --git a/Penumbra/Services/MigrationManager.cs b/Penumbra/Services/MigrationManager.cs index 2438c0ad..8db62e48 100644 --- a/Penumbra/Services/MigrationManager.cs +++ b/Penumbra/Services/MigrationManager.cs @@ -1,6 +1,10 @@ using Dalamud.Interface.ImGuiNotification; +using Lumina.Data.Files; using OtterGui.Classes; using OtterGui.Services; +using Lumina.Extensions; +using Penumbra.GameData.Files.Utility; +using Penumbra.Import.Textures; using SharpCompress.Common; using SharpCompress.Readers; using MdlFile = Penumbra.GameData.Files.MdlFile; @@ -296,6 +300,26 @@ public class MigrationManager(Configuration config) : IService } } + public void FixMipMaps(IReader reader, string directory, ExtractionOptions options) + { + var path = Path.Combine(directory, reader.Entry.Key!); + using var s = new MemoryStream(); + using var e = reader.OpenEntryStream(); + e.CopyTo(s); + var length = s.Position; + s.Seek(0, SeekOrigin.Begin); + var br = new BinaryReader(s, Encoding.UTF8, true); + var header = br.ReadStructure(); + br.Dispose(); + TexFileParser.FixMipOffsets(length, ref header, out var actualSize); + + s.Seek(0, SeekOrigin.Begin); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + using var f = File.Open(path, FileMode.Create, FileAccess.Write); + f.Write(header); + f.Write(s.GetBuffer().AsSpan(80, (int)actualSize - 80)); + } + /// Update the data of a .mdl file during TTMP extraction. Returns either the existing array or a new one. public byte[] MigrateTtmpModel(string path, byte[] data) { @@ -348,6 +372,25 @@ public class MigrationManager(Configuration config) : IService } } + public byte[] FixTtmpMipMaps(string path, byte[] data) + { + using var m = new MemoryStream(data); + var br = new BinaryReader(m, Encoding.UTF8, true); + var header = br.ReadStructure(); + br.Dispose(); + TexFileParser.FixMipOffsets(data.Length, ref header, out var actualSize); + if (actualSize == data.Length) + return data; + + var ret = new byte[actualSize]; + using var m2 = new MemoryStream(ret); + using var bw = new BinaryWriter(m2); + bw.Write(header); + bw.Write(data.AsSpan(80, (int)actualSize - 80)); + + return ret; + } + private static bool MigrateModel(string path, MdlFile mdl, bool createBackup) { From 973814b31b88c8c6176f1b12dc6a75a5d9439696 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Jun 2025 11:36:32 +0200 Subject: [PATCH 730/865] Some more BNPCs. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 94076bf6..a1252cdc 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 94076bf6bba27c02e0adbafa1c5cc9c279a0b5df +Subproject commit a1252cdcab09cbf4c9694971f29523f7485c90bc From 3d056623840592ffa8504577169270dac00bdad7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 8 Jun 2025 09:38:30 +0000 Subject: [PATCH 731/865] [CI] Updating repo.json for testing_1.4.0.3 --- repo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repo.json b/repo.json index e36176d5..a6cec268 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.2", + "TestingAssemblyVersion": "1.4.0.3", "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.2/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.4.0.3/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 a8c05fc6eea82e5beae224a1467a7f8255eda37d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Jun 2025 17:19:33 +0200 Subject: [PATCH 732/865] Make middle-mouse button handle temporary settings. --- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 2dff19ab..8a383791 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -229,14 +229,27 @@ public sealed class ModFileSystemSelector : FileSystemSelector Date: Fri, 13 Jun 2025 17:26:18 +0200 Subject: [PATCH 733/865] BNPCs. --- Penumbra.GameData | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index a1252cdc..10fdb025 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit a1252cdcab09cbf4c9694971f29523f7485c90bc +Subproject commit 10fdb025436f7ea9f1f5e97635c19eee0578de7b From 1f4ec984b348c66f962899721f3f97d34e6c098c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 13 Jun 2025 17:27:49 +0200 Subject: [PATCH 734/865] Use improved filesystem. --- OtterGui | 2 +- Penumbra/Api/Api/ModsApi.cs | 4 ++-- Penumbra/Mods/Manager/ModFileSystem.cs | 14 ++------------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/OtterGui b/OtterGui index 17a3ee57..78528f93 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 17a3ee5711ca30eb7f5b393dfb8136f0bce49b2b +Subproject commit 78528f93ac253db0061d9a8244cfa0cee5c2f873 diff --git a/Penumbra/Api/Api/ModsApi.cs b/Penumbra/Api/Api/ModsApi.cs index ace98f83..78c62953 100644 --- a/Penumbra/Api/Api/ModsApi.cs +++ b/Penumbra/Api/Api/ModsApi.cs @@ -112,7 +112,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName) { if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) + || !_modFileSystem.TryGetValue(mod, out var leaf)) return (PenumbraApiEc.ModMissing, string.Empty, false, false); var fullPath = leaf.FullName(); @@ -127,7 +127,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable return PenumbraApiEc.InvalidArgument; if (!_modManager.TryGetMod(modDirectory, modName, out var mod) - || !_modFileSystem.FindLeaf(mod, out var leaf)) + || !_modFileSystem.TryGetValue(mod, out var leaf)) return PenumbraApiEc.ModMissing; try diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 693db944..a5c46972 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -80,7 +80,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer // Update sort order when defaulted mod names change. private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) { - if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf)) + if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !TryGetValue(mod, out var leaf)) return; var old = oldName.FixName(); @@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer CreateDuplicateLeaf(parent, mod.Name.Text, mod); break; case ModPathChangeType.Deleted: - if (FindLeaf(mod, out var leaf)) + if (TryGetValue(mod, out var leaf)) Delete(leaf); break; @@ -124,16 +124,6 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable, ISer } } - // Search the entire filesystem for the leaf corresponding to a mod. - public bool FindLeaf(Mod mod, [NotNullWhen(true)] out Leaf? leaf) - { - leaf = Root.GetAllDescendants(ISortMode.Lexicographical) - .OfType() - .FirstOrDefault(l => l.Value == mod); - return leaf != null; - } - - // Used for saving and loading. private static string ModToIdentifier(Mod mod) => mod.ModPath.Name; From 1961b03d37f651a54a9746f245d6e0e4614188d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 15 Jun 2025 23:18:46 +0200 Subject: [PATCH 735/865] Fix issues with shapes and attributes with ID. --- .../Cache/ShapeAttributeHashSet.cs | 3 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 39 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs index e50ceaa2..4c61bdd2 100644 --- a/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs +++ b/Penumbra/Collections/Cache/ShapeAttributeHashSet.cs @@ -120,6 +120,7 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI if (TryGetValue((slot, id.Value), out var flags)) { + index *= 2; var newFlags = value switch { true => (flags | (1ul << index)) & ~(1ul << (index + 1)), @@ -137,7 +138,7 @@ public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryI if (value is null) return false; - this[(slot, id.Value)] = 1ul << (index + (value.Value ? 0 : 1)); + this[(slot, id.Value)] = 1ul << (2 * index + (value.Value ? 0 : 1)); return true; } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index a7bfd49c..7b940cd0 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -136,26 +136,33 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { if ((flags & 3) is not 0) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), (flags & 2) is not 0); - ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); - ImGui.SameLine(0, 0); + var enabled = (flags & 1) is 1; + + if (set[slot, GenderRace.Unknown] != enabled) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !enabled); + ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + ImGui.SameLine(0, 0); + } } else { - var currentFlags = flags >> 2; - var currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + var currentIndex = BitOperations.TrailingZeroCount(flags) / 2; + var currentFlags = flags >> (2 * currentIndex); while (currentIndex < ShapeAttributeHashSet.GenderRaceValues.Count) { - var value5 = (currentFlags & 1) is 1; - var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; - if (set[slot, gr] != value5) + var enabled = (currentFlags & 1) is 1; + var gr = ShapeAttributeHashSet.GenderRaceValues[currentIndex]; + if (set[slot, gr] != enabled) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !value5); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled), !enabled); ImUtf8.Text($"{gr.ToName()} {slot.ToName()} #{id.Id:D4}, "); + ImGui.SameLine(0, 0); } - currentFlags >>= currentIndex * 2; - currentIndex = BitOperations.TrailingZeroCount(currentFlags) / 2; + currentFlags &= ~0x3u; + currentIndex += BitOperations.TrailingZeroCount(currentFlags) / 2; + currentFlags = flags >> (2 * currentIndex); } } } @@ -167,7 +174,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##shapes"u8, 6, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##shapes"u8, 7, ImGuiTableFlags.RowBg); if (!table) return; @@ -175,6 +182,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("ID"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 4); ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Shapes"u8, ImGuiTableColumnFlags.WidthStretch); @@ -193,6 +201,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledShapeKeyIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{human.GetModelId((HumanSlot)i):D4}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); ImGui.TableNextColumn(); foreach (var ((shape, flag), idx) in model->ModelResourceHandle->Shapes.WithIndex()) @@ -211,6 +220,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } @@ -221,7 +231,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (!treeNode2) return; - using var table = ImUtf8.Table("##attributes"u8, 6, ImGuiTableFlags.RowBg); + using var table = ImUtf8.Table("##attributes"u8, 7, ImGuiTableFlags.RowBg); if (!table) return; @@ -229,6 +239,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImUtf8.TableSetupColumn("Slot"u8, ImGuiTableColumnFlags.WidthFixed, 150 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Address"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 14); ImUtf8.TableSetupColumn("Mask"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 8); + ImUtf8.TableSetupColumn("ID"u8, ImGuiTableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 4); ImUtf8.TableSetupColumn("Count"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImUtf8.GlobalScale); ImUtf8.TableSetupColumn("Attributes"u8, ImGuiTableColumnFlags.WidthStretch); @@ -247,6 +258,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) { var mask = model->EnabledAttributeIndexMask; ImUtf8.DrawTableColumn($"{mask:X8}"); + ImUtf8.DrawTableColumn($"{human.GetModelId((HumanSlot)i):D4}"); ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); ImGui.TableNextColumn(); foreach (var ((attribute, flag), idx) in model->ModelResourceHandle->Attributes.WithIndex()) @@ -265,6 +277,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); + ImGui.TableNextColumn(); } } } From 3c20b541ce5f5cd93ef5b1038407badb6ea4680c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 15 Jun 2025 23:20:13 +0200 Subject: [PATCH 736/865] Make mousewheel-scrolling work for setting combos, also filters. --- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 76 +++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 3e165cb5..566ec02c 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -15,13 +15,55 @@ using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab.Groups; -public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService +public sealed class ModGroupDrawer : IUiService { private readonly List<(IModGroup, int)> _blockGroupCache = []; private bool _temporary; private bool _locked; private TemporaryModSettings? _tempSettings; private ModSettings? _settings; + private readonly SingleGroupCombo _combo; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + + public ModGroupDrawer(Configuration config, CollectionManager collectionManager) + { + _config = config; + _collectionManager = collectionManager; + _combo = new SingleGroupCombo(this); + } + + private sealed class SingleGroupCombo(ModGroupDrawer parent) + : FilterComboCache(() => _group!.Options, MouseWheelType.Control, Penumbra.Log) + { + private static IModGroup? _group; + private static int _groupIdx; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var option = _group!.Options[globalIdx]; + var ret = ImUtf8.Selectable(option.Name, globalIdx == CurrentSelectionIdx); + + if (option.Description.Length > 0) + ImUtf8.SelectableHelpMarker(option.Description); + + return ret; + } + + protected override string ToString(IModOption obj) + => obj.Name; + + public void Draw(IModGroup group, int groupIndex, int currentOption) + { + _group = group; + _groupIdx = groupIndex; + CurrentSelectionIdx = currentOption; + CurrentSelection = _group.Options[CurrentSelectionIdx]; + if (Draw(string.Empty, CurrentSelection.Name, string.Empty, ref CurrentSelectionIdx, UiHelpers.InputTextWidth.X * 3 / 4, + ImGui.GetTextLineHeightWithSpacing())) + parent.SetModSetting(_group, _groupIdx, Setting.Single(CurrentSelectionIdx)); + } + } public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { @@ -41,7 +83,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle switch (group.Behaviour) { - case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax: + case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax: case GroupDrawBehaviour.MultiSelection: _blockGroupCache.Add((group, idx)); break; @@ -76,25 +118,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle using var id = ImUtf8.PushId(groupIdx); var selectedOption = setting.AsIndex; using var disabled = ImRaii.Disabled(_locked); - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - var options = group.Options; - using (var combo = ImUtf8.Combo(""u8, options[selectedOption].Name)) - { - if (combo) - for (var idx2 = 0; idx2 < options.Count; ++idx2) - { - id.Push(idx2); - var option = options[idx2]; - if (ImUtf8.Selectable(option.Name, idx2 == selectedOption)) - SetModSetting(group, groupIdx, Setting.Single(idx2)); - - if (option.Description.Length > 0) - ImUtf8.SelectableHelpMarker(option.Description); - - id.Pop(); - } - } - + _combo.Draw(group, groupIdx, selectedOption); ImGui.SameLine(); if (group.Description.Length > 0) ImUtf8.LabeledHelpMarker(group.Name, group.Description); @@ -195,7 +219,7 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) { - if (options.Count <= config.OptionGroupCollapsibleMin) + if (options.Count <= _config.OptionGroupCollapsibleMin) { draw(); } @@ -240,21 +264,21 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } private ModCollection Current - => collectionManager.Active.Current; + => _collectionManager.Active.Current; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) { - if (_temporary || config.DefaultTemporaryMode) + if (_temporary || _config.DefaultTemporaryMode) { _tempSettings ??= new TemporaryModSettings(group.Mod, _settings); _tempSettings!.ForceInherit = false; _tempSettings!.Settings[groupIdx] = setting; - collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); + _collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); } else { - collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + _collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); } } } From 9fc572ba0cb198a3acbf0bd98a696e243c2bc433 Mon Sep 17 00:00:00 2001 From: Actions User Date: Sun, 15 Jun 2025 21:47:41 +0000 Subject: [PATCH 737/865] [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 738/865] 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 739/865] 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 740/865] 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 741/865] 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 742/865] 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 743/865] [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 744/865] 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 745/865] 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 746/865] [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 747/865] 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 748/865] 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 749/865] 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 750/865] 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 751/865] 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 752/865] 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 753/865] 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 754/865] 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 755/865] 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 756/865] 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 757/865] 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 758/865] 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 759/865] 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 760/865] 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 761/865] 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 762/865] 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 763/865] 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 764/865] [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 765/865] 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 766/865] 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 767/865] 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 768/865] 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 769/865] 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 770/865] 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 771/865] [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 772/865] 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 773/865] [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 774/865] 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 775/865] 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 776/865] 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 777/865] [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 778/865] 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 779/865] [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 780/865] 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 781/865] [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 782/865] 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 783/865] 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 784/865] 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 785/865] 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 786/865] [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 787/865] 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 788/865] 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 789/865] 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 790/865] 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 791/865] 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 792/865] 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 793/865] [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 794/865] 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 795/865] 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 796/865] [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 797/865] 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 798/865] 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 799/865] 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 800/865] 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 801/865] [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 802/865] 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 803/865] 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 804/865] 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 805/865] [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 806/865] 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 807/865] 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 808/865] [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 809/865] 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 810/865] 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 811/865] 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 812/865] 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 813/865] 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 814/865] 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 815/865] 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 816/865] 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 817/865] 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 818/865] 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 819/865] 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 820/865] 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 821/865] 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 822/865] [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 823/865] 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 824/865] 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 825/865] 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 826/865] 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 827/865] [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 828/865] 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 829/865] [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 830/865] 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 831/865] [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 832/865] 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 833/865] 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 834/865] [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 835/865] 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 836/865] 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 837/865] 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 838/865] 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 839/865] 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 840/865] 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 841/865] [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 842/865] 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 5be021b0eb248eede38e8d205bc75bd95b2305df Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 13 Nov 2025 19:53:50 +0100 Subject: [PATCH 843/865] Add integration settings sections --- Penumbra.Api | 2 +- Penumbra/Api/Api/PenumbraApi.cs | 2 +- Penumbra/Api/Api/UiApi.cs | 26 +++-- Penumbra/Api/IpcProviders.cs | 2 + .../IntegrationSettingsRegistry.cs | 110 ++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 8 +- 6 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 Penumbra/UI/Integration/IntegrationSettingsRegistry.cs diff --git a/Penumbra.Api b/Penumbra.Api index 874a3773..b97784bd 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 874a3773bc4f637de1ef1fa8756b4debe3d8f68b +Subproject commit b97784bd7cd911bd0a323cd8e717714de1875469 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/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index b14f67ae..6fb116f3 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -5,20 +5,24 @@ using Penumbra.GameData.Data; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; +using Penumbra.UI.Integration; +using Penumbra.UI.Tabs; namespace Penumbra.Api.Api; public class UiApi : IPenumbraApiUi, IApiService, IDisposable { - private readonly CommunicatorService _communicator; - private readonly ConfigWindow _configWindow; - private readonly ModManager _modManager; + private readonly CommunicatorService _communicator; + private readonly ConfigWindow _configWindow; + private readonly ModManager _modManager; + private readonly IntegrationSettingsRegistry _integrationSettings; - public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager) + public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager, IntegrationSettingsRegistry integrationSettings) { - _communicator = communicator; - _configWindow = configWindow; - _modManager = modManager; + _communicator = communicator; + _configWindow = configWindow; + _modManager = modManager; + _integrationSettings = integrationSettings; _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); } @@ -98,4 +102,12 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable var (type, id) = data.ToApiObject(); ChangedItemTooltip.Invoke(type, id); } + + public PenumbraApiEc RegisterSettingsSection(Action draw) + => _integrationSettings.RegisterSection(draw); + + public PenumbraApiEc UnregisterSettingsSection(Action draw) + => _integrationSettings.UnregisterSection(draw) + ? PenumbraApiEc.Success + : PenumbraApiEc.NothingChanged; } diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 5f04540f..197cf3d2 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -130,6 +130,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.PostSettingsDraw.Provider(pi, api.Ui), IpcSubscribers.OpenMainWindow.Provider(pi, api.Ui), IpcSubscribers.CloseMainWindow.Provider(pi, api.Ui), + IpcSubscribers.RegisterSettingsSection.Provider(pi, api.Ui), + IpcSubscribers.UnregisterSettingsSection.Provider(pi, api.Ui), ]; if (_characterUtility.Ready) _initializedProvider.Invoke(); diff --git a/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs new file mode 100644 index 00000000..ab26a68f --- /dev/null +++ b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs @@ -0,0 +1,110 @@ +using Dalamud.Plugin; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Enums; + +namespace Penumbra.UI.Integration; + +public sealed class IntegrationSettingsRegistry : IService, IDisposable +{ + private readonly IDalamudPluginInterface _pluginInterface; + + private readonly List<(string InternalName, string Name, Action Draw)> _sections = []; + + private bool _disposed = false; + + public IntegrationSettingsRegistry(IDalamudPluginInterface pluginInterface) + { + _pluginInterface = pluginInterface; + + _pluginInterface.ActivePluginsChanged += OnActivePluginsChanged; + } + + public void Dispose() + { + _disposed = true; + + _pluginInterface.ActivePluginsChanged -= OnActivePluginsChanged; + + _sections.Clear(); + } + + public void Draw() + { + foreach (var (internalName, name, draw) in _sections) + { + if (!ImUtf8.CollapsingHeader($"Integration with {name}###IntegrationSettingsHeader.{internalName}")) + continue; + + using var id = ImUtf8.PushId($"IntegrationSettings.{internalName}"); + try + { + draw(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error while drawing {internalName} integration settings: {e}"); + } + } + } + + public PenumbraApiEc RegisterSection(Action draw) + { + if (_disposed) + return PenumbraApiEc.SystemDisposed; + + var plugin = GetPlugin(draw); + if (plugin is null) + return PenumbraApiEc.InvalidArgument; + + var section = (plugin.InternalName, plugin.Name, draw); + + var index = FindSectionIndex(plugin.InternalName); + if (index >= 0) + { + if (_sections[index] == section) + return PenumbraApiEc.NothingChanged; + _sections[index] = section; + } + else + _sections.Add(section); + _sections.Sort((lhs, rhs) => string.Compare(lhs.Name, rhs.Name, StringComparison.CurrentCultureIgnoreCase)); + + return PenumbraApiEc.Success; + } + + public bool UnregisterSection(Action draw) + { + var index = FindSectionIndex(draw); + if (index < 0) + return false; + + _sections.RemoveAt(index); + return true; + } + + private void OnActivePluginsChanged(IActivePluginsChangedEventArgs args) + { + if (args.Kind is PluginListInvalidationKind.Loaded) + return; + + foreach (var internalName in args.AffectedInternalNames) + { + var index = FindSectionIndex(internalName); + if (index >= 0 && GetPlugin(_sections[index].Draw) is null) + { + _sections.RemoveAt(index); + Penumbra.Log.Warning($"Removed stale integration setting section of {internalName} (reason: {args.Kind})"); + } + } + } + + private IExposedPlugin? GetPlugin(Delegate @delegate) + => null; // TODO Use IDalamudPluginInterface.GetPlugin(Assembly) when it's in Dalamud stable. + + private int FindSectionIndex(string internalName) + => _sections.FindIndex(section => section.InternalName.Equals(internalName, StringComparison.Ordinal)); + + private int FindSectionIndex(Action draw) + => _sections.FindIndex(section => section.Draw == draw); +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 86c01cb2..09c7c58d 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -20,6 +20,7 @@ using Penumbra.Interop.Services; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.Integration; using Penumbra.UI.ModsTab; namespace Penumbra.UI.Tabs; @@ -55,6 +56,7 @@ public class SettingsTab : ITab, IUiService private readonly CleanupService _cleanupService; private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; + private readonly IntegrationSettingsRegistry _integrationSettings; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -71,7 +73,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, PcpService pcpService) + AttributeHook attributeHook, PcpService pcpService, IntegrationSettingsRegistry integrationSettings) { _pluginInterface = pluginInterface; _config = config; @@ -99,6 +101,7 @@ public class SettingsTab : ITab, IUiService _cleanupService = cleanupService; _attributeHook = attributeHook; _pcpService = pcpService; + _integrationSettings = integrationSettings; } public void DrawHeader() @@ -129,6 +132,7 @@ public class SettingsTab : ITab, IUiService DrawColorSettings(); DrawPredefinedTagsSection(); DrawAdvancedSettings(); + _integrationSettings.Draw(); DrawSupportButtons(); } @@ -1133,7 +1137,7 @@ public class SettingsTab : ITab, IUiService } #endregion - + /// Draw the support button group on the right-hand side of the window. private void DrawSupportButtons() { From e240a42a2ccd35d06be417033d034448c5c8be35 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 13 Nov 2025 19:55:32 +0100 Subject: [PATCH 844/865] Replace GetPlugin(Delegate) stub by actual implementation --- Penumbra/UI/Integration/IntegrationSettingsRegistry.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs index ab26a68f..2d3da488 100644 --- a/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs +++ b/Penumbra/UI/Integration/IntegrationSettingsRegistry.cs @@ -100,7 +100,12 @@ public sealed class IntegrationSettingsRegistry : IService, IDisposable } private IExposedPlugin? GetPlugin(Delegate @delegate) - => null; // TODO Use IDalamudPluginInterface.GetPlugin(Assembly) when it's in Dalamud stable. + => @delegate.Method.DeclaringType + switch + { + null => null, + var type => _pluginInterface.GetPlugin(type.Assembly), + }; private int FindSectionIndex(string internalName) => _sections.FindIndex(section => section.InternalName.Equals(internalName, StringComparison.Ordinal)); From 338e3bc1a5107267aace6ae9b1a89e30a7e7a757 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Thu, 20 Nov 2025 18:41:32 +0100 Subject: [PATCH 845/865] Update Penumbra.Api --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index b97784bd..704d62f6 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit b97784bd7cd911bd0a323cd8e717714de1875469 +Subproject commit 704d62f64f791b8cfd42363beaa464ad6f98ae48 From 5dd74297c623430ea63ad7a01531ba9b58e75eb7 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 28 Nov 2025 22:10:17 +0000 Subject: [PATCH 846/865] [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 847/865] 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); } } }); From 3e7511cb348d936736621b9152ae54772e58ef21 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 17 Dec 2025 18:33:10 +0100 Subject: [PATCH 848/865] Update SDK. --- OtterGui | 2 +- Penumbra.Api | 2 +- .../Penumbra.CrashHandler.csproj | 2 +- Penumbra.CrashHandler/packages.lock.json | 8 ++--- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/UI/CollectionTab/CollectionPanel.cs | 2 +- Penumbra/UI/Tabs/CollectionsTab.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 3 +- Penumbra/UI/Tabs/ModsTab.cs | 3 +- Penumbra/UI/Tabs/ResourceTab.cs | 2 +- Penumbra/packages.lock.json | 36 ++++--------------- 13 files changed, 23 insertions(+), 45 deletions(-) diff --git a/OtterGui b/OtterGui index a63f6735..6f323645 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a63f6735cf4bed4f7502a022a10378607082b770 +Subproject commit 6f3236453b1edfaa25c8edcc8b39a9d9b2fc18ac diff --git a/Penumbra.Api b/Penumbra.Api index 3d6cee1a..e4934ccc 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf +Subproject commit e4934ccca0379f22dadf989ab2d34f30b3c5c7ea diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index 1b1f0a28..4c864d39 100644 --- a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj +++ b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/Penumbra.CrashHandler/packages.lock.json b/Penumbra.CrashHandler/packages.lock.json index 1d395083..0a160ea5 100644 --- a/Penumbra.CrashHandler/packages.lock.json +++ b/Penumbra.CrashHandler/packages.lock.json @@ -1,12 +1,12 @@ { "version": 1, "dependencies": { - "net9.0-windows7.0": { + "net10.0-windows7.0": { "DotNet.ReproducibleBuilds": { "type": "Direct", - "requested": "[1.2.25, )", - "resolved": "1.2.25", - "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" } } } diff --git a/Penumbra.GameData b/Penumbra.GameData index d889f9ef..2ff50e68 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit d889f9ef918514a46049725052d378b441915b00 +Subproject commit 2ff50e68f7c951f0f8b25957a400a2e32ed9d6dc diff --git a/Penumbra.String b/Penumbra.String index c8611a0c..0315144a 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793 +Subproject commit 0315144ab5614c11911e2a4dddf436fb18c5d7e3 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index fa45ffbf..f04928a5 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -1,4 +1,4 @@ - + Penumbra absolute gangstas diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index e41ceade..7a8ca032 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.Services; using OtterGui; using OtterGui.Classes; using OtterGui.Extensions; @@ -17,7 +18,6 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; -using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index f2a041eb..b458fc16 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 Dalamud.Plugin.Services; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Widgets; diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 05f77e29..c7f0635d 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.Game.ClientState.Objects.SubKinds; using Dalamud.Interface.Colors; using Microsoft.Extensions.DependencyInjection; using OtterGui; @@ -1033,7 +1034,7 @@ public class DebugTab : Window, ITab, IUiService /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { - var player = _clientState.LocalPlayer; + var player = _objects.Objects.LocalPlayer; var name = player?.Name.ToString() ?? "NULL"; if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) return; diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 79dcbb9e..1d24c597 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -27,7 +27,6 @@ public class ModsTab( TutorialService tutorial, RedrawService redrawService, Configuration config, - IClientState clientState, CollectionSelectHeader collectionHeader, ITargetManager targets, ObjectManager objects) @@ -113,7 +112,7 @@ public class ModsTab( ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); using var id = ImRaii.PushId("Redraw"); - using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null); + using var disabled = ImRaii.Disabled(objects.Objects.LocalPlayer is null); ImGui.SameLine(); var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 }; var tt = !objects[0].Valid diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 593adde1..2223075d 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -1,5 +1,5 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Game; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.Interop; diff --git a/Penumbra/packages.lock.json b/Penumbra/packages.lock.json index 7499bffa..c904870a 100644 --- a/Penumbra/packages.lock.json +++ b/Penumbra/packages.lock.json @@ -1,12 +1,12 @@ { "version": 1, "dependencies": { - "net9.0-windows7.0": { + "net10.0-windows7.0": { "DotNet.ReproducibleBuilds": { "type": "Direct", - "requested": "[1.2.25, )", - "resolved": "1.2.25", - "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" + "requested": "[1.2.39, )", + "resolved": "1.2.39", + "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" }, "EmbedIO": { "type": "Direct", @@ -33,7 +33,6 @@ "resolved": "0.40.0", "contentHash": "yP/aFX1jqGikVF7u2f05VEaWN4aCaKNLxSas82UgA2GGVECxq/BcqZx3STHCJ78qilo1azEOk1XpBglIuGMb7w==", "dependencies": { - "System.Buffers": "4.6.0", "ZstdSharp.Port": "0.8.5" } }, @@ -66,10 +65,7 @@ "FlatSharp.Runtime": { "type": "Transitive", "resolved": "7.9.0", - "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==", - "dependencies": { - "System.Memory": "4.5.5" - } + "contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==" }, "JetBrains.Annotations": { "type": "Transitive", @@ -102,33 +98,15 @@ "SharpGLTF.Core": "1.0.5" } }, - "System.Buffers": { - "type": "Transitive", - "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", "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" }, - "System.ValueTuple": { - "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" - }, "Unosquare.Swan.Lite": { "type": "Transitive", "resolved": "3.1.0", - "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==", - "dependencies": { - "System.ValueTuple": "4.5.0" - } + "contentHash": "X3s5QE/KMj3WAPFqFve7St+Ds10BB50u8kW8PmKIn7FVkn7yEXe9Yxr2htt1WV85DRqfFR0MN/BUNHkGHtL4OQ==" }, "ZstdSharp.Port": { "type": "Transitive", @@ -154,7 +132,7 @@ "FlatSharp.Compiler": "[7.9.0, )", "FlatSharp.Runtime": "[7.9.0, )", "OtterGui": "[1.0.0, )", - "Penumbra.Api": "[5.10.0, )", + "Penumbra.Api": "[5.13.0, )", "Penumbra.String": "[1.0.6, )" } }, From 7717251c6a1184075b2a685def2a44c33a9bfdff Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 18 Dec 2025 20:45:15 +0100 Subject: [PATCH 849/865] Update to TerraFX. --- Penumbra.GameData | 2 +- Penumbra/Api/Api/ModSettingsApi.cs | 21 ++++ Penumbra/Api/Api/ResolveApi.cs | 34 +++++ Penumbra/Api/Api/UiApi.cs | 6 + Penumbra/Api/IpcProviders.cs | 3 + Penumbra/Import/Textures/TextureManager.cs | 85 ++++++++++--- .../Interop/Services/TextureArraySlicer.cs | 116 +++++++++++------- Penumbra/Penumbra.csproj | 12 +- Penumbra/Services/StaticServiceManager.cs | 1 + 9 files changed, 211 insertions(+), 69 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 2ff50e68..3d4d8510 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 2ff50e68f7c951f0f8b25957a400a2e32ed9d6dc +Subproject commit 3d4d8510f832dfd95d7069b86e6b3da4ec612558 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 3ba17cf4..d49c2904 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -73,6 +73,27 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return (ret.Item1, (ret.Item2.Value.Item1, ret.Item2.Value.Item2, ret.Item2.Value.Item3, ret.Item2.Value.Item4)); } + public PenumbraApiEc GetSettingsInAllCollections(string modDirectory, string modName, + out Dictionary>, bool, bool)> settings, + bool ignoreTemporaryCollections = false) + { + settings = []; + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return PenumbraApiEc.ModMissing; + + var collections = ignoreTemporaryCollections + ? _collectionManager.Storage.Where(c => c != ModCollection.Empty) + : _collectionManager.Storage.Where(c => c != ModCollection.Empty).Concat(_collectionManager.Temp.Values); + settings = []; + foreach (var collection in collections) + { + if (GetCurrentSettings(collection, mod, false, false, 0) is { } s) + settings.Add(collection.Identity.Id, s); + } + + return PenumbraApiEc.Success; + } + public (PenumbraApiEc, (bool, int, Dictionary>, bool, bool)?) GetCurrentModSettingsWithTemp(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance, bool ignoreTemporary, int key) { diff --git a/Penumbra/Api/Api/ResolveApi.cs b/Penumbra/Api/Api/ResolveApi.cs index 481ea7ad..00a0c86f 100644 --- a/Penumbra/Api/Api/ResolveApi.cs +++ b/Penumbra/Api/Api/ResolveApi.cs @@ -1,5 +1,6 @@ using Dalamud.Plugin.Services; using OtterGui.Services; +using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Interop.PathResolving; @@ -41,6 +42,19 @@ public class ResolveApi( return ret.Select(r => r.ToString()).ToArray(); } + public PenumbraApiEc ResolvePath(Guid collectionId, string gamePath, out string resolvedPath) + { + resolvedPath = gamePath; + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return PenumbraApiEc.CollectionMissing; + + if (!collection.HasCache) + return PenumbraApiEc.CollectionInactive; + + resolvedPath = ResolvePath(gamePath, modManager, collection); + return PenumbraApiEc.Success; + } + public string[] ReverseResolvePlayerPath(string moddedPath) { if (!config.EnableMods) @@ -64,6 +78,26 @@ public class ResolveApi( return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } + public PenumbraApiEc ResolvePaths(Guid collectionId, string[] forward, string[] reverse, out string[] resolvedForward, + out string[][] resolvedReverse) + { + resolvedForward = forward; + resolvedReverse = []; + if (!config.EnableMods) + return PenumbraApiEc.Success; + + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return PenumbraApiEc.CollectionMissing; + + if (!collection.HasCache) + return PenumbraApiEc.CollectionInactive; + + resolvedForward = forward.Select(p => ResolvePath(p, modManager, collection)).ToArray(); + var reverseResolved = collection.ReverseResolvePaths(reverse); + resolvedReverse = reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray(); + return PenumbraApiEc.Success; + } + public async Task<(string[], string[][])> ResolvePlayerPathsAsync(string[] forward, string[] reverse) { if (!config.EnableMods) diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index b14f67ae..70f018bb 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -81,6 +81,12 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; + public PenumbraApiEc RegisterSettingsSection(Action draw) + => throw new NotImplementedException(); + + public PenumbraApiEc UnregisterSettingsSection(Action draw) + => throw new NotImplementedException(); + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 5f04540f..fdacc73b 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -66,6 +66,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.GetCurrentModSettings.Provider(pi, api.ModSettings), IpcSubscribers.GetCurrentModSettingsWithTemp.Provider(pi, api.ModSettings), IpcSubscribers.GetAllModSettings.Provider(pi, api.ModSettings), + IpcSubscribers.GetSettingsInAllCollections.Provider(pi, api.ModSettings), IpcSubscribers.TryInheritMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetMod.Provider(pi, api.ModSettings), IpcSubscribers.TrySetModPriority.Provider(pi, api.ModSettings), @@ -98,6 +99,8 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ReverseResolvePlayerPath.Provider(pi, api.Resolve), IpcSubscribers.ResolvePlayerPaths.Provider(pi, api.Resolve), IpcSubscribers.ResolvePlayerPathsAsync.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePath.Provider(pi, api.Resolve), + IpcSubscribers.ResolvePaths.Provider(pi, api.Resolve), IpcSubscribers.GetGameObjectResourcePaths.Provider(pi, api.ResourceTree), IpcSubscribers.GetPlayerResourcePaths.Provider(pi, api.ResourceTree), diff --git a/Penumbra/Import/Textures/TextureManager.cs b/Penumbra/Import/Textures/TextureManager.cs index 073fef2f..177722ec 100644 --- a/Penumbra/Import/Textures/TextureManager.cs +++ b/Penumbra/Import/Textures/TextureManager.cs @@ -7,12 +7,12 @@ using OtterGui.Log; using OtterGui.Services; using OtterGui.Tasks; using OtterTex; -using SharpDX.Direct3D11; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.PixelFormats; -using DxgiDevice = SharpDX.DXGI.Device; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; using Image = SixLabors.ImageSharp.Image; namespace Penumbra.Import.Textures; @@ -125,11 +125,11 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur switch (_type) { case TextureType.Png: - data?.SaveAsync(_outputPath, new PngEncoder() { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) + data?.SaveAsync(_outputPath, new PngEncoder { CompressionLevel = PngCompressionLevel.NoCompression }, cancel) .Wait(cancel); return; case TextureType.Targa: - data?.SaveAsync(_outputPath, new TgaEncoder() + data?.SaveAsync(_outputPath, new TgaEncoder { Compression = TgaCompression.None, BitsPerPixel = TgaBitsPerPixel.Pixel32, @@ -204,11 +204,16 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur rgba, width, height), CombinedTexture.TextureSaveType.AsIs when imageTypeBehaviour is TextureType.Dds => AddMipMaps(image.AsDds!, _mipMaps), CombinedTexture.TextureSaveType.Bitmap => ConvertToRgbaDds(image, _mipMaps, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, width, height), - CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, width, height), + CombinedTexture.TextureSaveType.BC1 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC1UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC3 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC3UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC4 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC4UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC5 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC5UNorm, cancel, rgba, + width, height), + CombinedTexture.TextureSaveType.BC7 => _textures.ConvertToCompressedDds(image, _mipMaps, DXGIFormat.BC7UNorm, cancel, rgba, + width, height), _ => throw new Exception("Wrong save type."), }; @@ -390,7 +395,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur } /// Create a BC3 or BC7 block-compressed .dds from the input (optionally with mipmaps). Returns input (+ mipmaps) if it is already the correct format. - public ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) + public unsafe ScratchImage CreateCompressed(ScratchImage input, bool mipMaps, DXGIFormat format, CancellationToken cancel) { if (input.Meta.Format == format) return input; @@ -406,11 +411,58 @@ 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 = new Device(uiBuilder.DeviceHandle); - var dxgiDevice = device.QueryInterface(); + ref var device = ref *(ID3D11Device*)uiBuilder.DeviceHandle; + IDXGIDevice* dxgiDevice; + Marshal.ThrowExceptionForHR(device.QueryInterface(TerraFX.Interop.Windows.Windows.__uuidof(), (void**)&dxgiDevice)); - using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel); - return input.Compress(deviceClone.NativePointer, format, CompressFlags.Parallel); + try + { + IDXGIAdapter* adapter = null; + Marshal.ThrowExceptionForHR(dxgiDevice->GetAdapter(&adapter)); + try + { + dxgiDevice->Release(); + dxgiDevice = null; + + ID3D11Device* deviceClone = null; + ID3D11DeviceContext* contextClone = null; + var featureLevel = device.GetFeatureLevel(); + Marshal.ThrowExceptionForHR(DirectX.D3D11CreateDevice( + adapter, + D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, + HMODULE.NULL, + device.GetCreationFlags(), + &featureLevel, + 1, + D3D11.D3D11_SDK_VERSION, + &deviceClone, + null, + &contextClone)); + try + { + adapter->Release(); + adapter = null; + return input.Compress((nint)deviceClone, format, CompressFlags.Parallel); + } + finally + { + if (contextClone is not null) + contextClone->Release(); + if (deviceClone is not null) + deviceClone->Release(); + } + } + finally + { + if (adapter is not null) + adapter->Release(); + } + } + finally + { + if (dxgiDevice is not null) + dxgiDevice->Release(); + } } return input.Compress(format, CompressFlags.BC7Quick | CompressFlags.Parallel); @@ -456,7 +508,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur GC.KeepAlive(input); } - private readonly struct ImageInputData + private readonly struct ImageInputData : IEquatable { private readonly string? _inputPath; @@ -524,5 +576,8 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur public override int GetHashCode() => _inputPath != null ? _inputPath.ToLowerInvariant().GetHashCode() : HashCode.Combine(_width, _height); + + public override bool Equals(object? obj) + => obj is ImageInputData o && Equals(o); } } diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index 11498878..7b873f26 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -1,8 +1,7 @@ using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using OtterGui.Services; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; +using TerraFX.Interop.DirectX; namespace Penumbra.Interop.Services; @@ -22,46 +21,78 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (texture == null) throw new ArgumentNullException(nameof(texture)); if (sliceIndex >= texture->ArraySize) - throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + throw new ArgumentOutOfRangeException(nameof(sliceIndex), + $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})"); + if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state)) { state.Refresh(); return new ImTextureID((nint)state.ShaderResourceView); } - var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView; - var description = srv.Description; - switch (description.Dimension) + + ref var srv = ref *(ID3D11ShaderResourceView*)(nint)texture->D3D11ShaderResourceView; + srv.AddRef(); + try { - case ShaderResourceViewDimension.Texture1D: - case ShaderResourceViewDimension.Texture2D: - case ShaderResourceViewDimension.Texture2DMultisampled: - case ShaderResourceViewDimension.Texture3D: - case ShaderResourceViewDimension.TextureCube: - // This function treats these as single-slice arrays. - // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. - break; - case ShaderResourceViewDimension.Texture1DArray: - description.Texture1DArray.FirstArraySlice = sliceIndex; - description.Texture2DArray.ArraySize = 1; - break; - case ShaderResourceViewDimension.Texture2DArray: - description.Texture2DArray.FirstArraySlice = sliceIndex; - description.Texture2DArray.ArraySize = 1; - break; - case ShaderResourceViewDimension.Texture2DMultisampledArray: - description.Texture2DMSArray.FirstArraySlice = sliceIndex; - description.Texture2DMSArray.ArraySize = 1; - break; - case ShaderResourceViewDimension.TextureCubeArray: - description.TextureCubeArray.First2DArrayFace = sliceIndex * 6; - description.TextureCubeArray.CubeCount = 1; - break; - default: - throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}"); + D3D11_SHADER_RESOURCE_VIEW_DESC description; + srv.GetDesc(&description); + switch (description.ViewDimension) + { + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1D: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DMS: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE3D: + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURECUBE: + // This function treats these as single-slice arrays. + // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do. + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1DARRAY: + description.Texture1DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DARRAY: + description.Texture2DArray.FirstArraySlice = sliceIndex; + description.Texture2DArray.ArraySize = 1; + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DMSARRAY: + description.Texture2DMSArray.FirstArraySlice = sliceIndex; + description.Texture2DMSArray.ArraySize = 1; + break; + case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURECUBEARRAY: + description.TextureCubeArray.First2DArrayFace = sliceIndex * 6u; + description.TextureCubeArray.NumCubes = 1; + break; + default: + throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.ViewDimension}"); + } + + ID3D11Device* device = null; + srv.GetDevice(&device); + ID3D11Resource* resource = null; + srv.GetResource(&resource); + try + { + ID3D11ShaderResourceView* slicedSrv = null; + Marshal.ThrowExceptionForHR(device->CreateShaderResourceView(resource, &description, &slicedSrv)); + resource->Release(); + device->Release(); + + state = new SliceState(slicedSrv); + _activeSlices.Add(((nint)texture, sliceIndex), state); + return new ImTextureID((nint)state.ShaderResourceView); + } + finally + { + if (resource is not null) + resource->Release(); + if (device is not null) + device->Release(); + } + } + finally + { + srv.Release(); } - state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description)); - _activeSlices.Add(((nint)texture, sliceIndex), state); - return new ImTextureID((nint)state.ShaderResourceView); } public void Tick() @@ -73,10 +104,9 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (!slice.Tick()) _expiredKeys.Add(key); } + foreach (var key in _expiredKeys) - { _activeSlices.Remove(key); - } } finally { @@ -87,14 +117,12 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable public void Dispose() { foreach (var slice in _activeSlices.Values) - { slice.Dispose(); - } } - private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable + private sealed class SliceState(ID3D11ShaderResourceView* shaderResourceView) : IDisposable { - public readonly ShaderResourceView ShaderResourceView = shaderResourceView; + public readonly ID3D11ShaderResourceView* ShaderResourceView = shaderResourceView; private uint _timeToLive = InitialTimeToLive; @@ -108,13 +136,15 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable if (unchecked(_timeToLive--) > 0) return true; - ShaderResourceView.Dispose(); + if (ShaderResourceView is not null) + ShaderResourceView->Release(); return false; } public void Dispose() { - ShaderResourceView.Dispose(); + if (ShaderResourceView is not null) + ShaderResourceView->Release(); } } } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index f04928a5..43f853f3 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -38,16 +38,8 @@ $(DalamudLibPath)Iced.dll False - - $(DalamudLibPath)SharpDX.dll - False - - - $(DalamudLibPath)SharpDX.Direct3D11.dll - False - - - $(DalamudLibPath)SharpDX.DXGI.dll + + $(DalamudLibPath)TerraFX.Interop.Windows.dll False diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 27582395..be482d1d 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -48,6 +48,7 @@ public static class StaticServiceManager .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) + .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) .AddDalamudService(pi) From 4c8ff408211eda482cd3978c5ec502c19e6d48b6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 18 Dec 2025 20:47:49 +0100 Subject: [PATCH 850/865] Fix private Unks. --- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 9 +++++---- Penumbra/Util/PointerExtensions.cs | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 Penumbra/Util/PointerExtensions.cs diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index f0ab1125..bc5f0765 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -9,6 +9,7 @@ using OtterGui.Text; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String; +using Penumbra.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; namespace Penumbra.UI.Tabs.Debug; @@ -178,10 +179,10 @@ public unsafe class GlobalVariablesDrawer( if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMapU8.Span) >= 0) { ImUtf8.DrawTableColumn($"[{total:D4}]"); - ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn($"{resource->Name.GetField(16)}"); // Unk1 ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); ImUtf8.DrawTableColumn($"{resource->Consumers}"); - ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImUtf8.DrawTableColumn($"{PointerExtensions.GetField(resource, 120)}"); // key, Unk1 ImGui.TableNextColumn(); Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); @@ -227,10 +228,10 @@ public unsafe class GlobalVariablesDrawer( if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) { ImUtf8.DrawTableColumn($"[{total:D4}]"); - ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); + ImUtf8.DrawTableColumn($"{resource->Name.GetField(16)}"); // Unk1 ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); ImUtf8.DrawTableColumn($"{resource->Consumers}"); - ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key + ImUtf8.DrawTableColumn($"{PointerExtensions.GetField(resource, 120)}"); // key, Unk1 ImGui.TableNextColumn(); Penumbra.Dynamis.DrawPointer(resource); ImGui.TableNextColumn(); diff --git a/Penumbra/Util/PointerExtensions.cs b/Penumbra/Util/PointerExtensions.cs new file mode 100644 index 00000000..c70e2177 --- /dev/null +++ b/Penumbra/Util/PointerExtensions.cs @@ -0,0 +1,20 @@ +namespace Penumbra.Util; + +public static class PointerExtensions +{ + public static unsafe ref TField GetField(this ref TPointer reference, int offset) + where TPointer : unmanaged + where TField : unmanaged + { + var pointer = (byte*)Unsafe.AsPointer(ref reference) + offset; + return ref *(TField*)pointer; + } + + public static unsafe ref TField GetField(TPointer* itemPointer, int offset) + where TPointer : unmanaged + where TField : unmanaged + { + var pointer = (byte*)itemPointer + offset; + return ref *(TField*)pointer; + } +} From febced07080a84e7526e56b1944128ecd6dc8d9a Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:47:19 +0100 Subject: [PATCH 851/865] Fix bug in slicer. --- Penumbra/Interop/Services/TextureArraySlicer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index 7b873f26..3cd57a33 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -48,7 +48,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable break; case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE1DARRAY: description.Texture1DArray.FirstArraySlice = sliceIndex; - description.Texture2DArray.ArraySize = 1; + description.Texture1DArray.ArraySize = 1; break; case D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2DARRAY: description.Texture2DArray.FirstArraySlice = sliceIndex; From ebcbc5d98a896c96756588c916aa52294a91773d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:51:39 +0100 Subject: [PATCH 852/865] Update SDK. --- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.CrashHandler/Penumbra.CrashHandler.csproj | 2 +- Penumbra.GameData | 2 +- Penumbra.String | 2 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/Penumbra.json | 2 +- repo.json | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/OtterGui b/OtterGui index 6f323645..ff1e6543 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 6f3236453b1edfaa25c8edcc8b39a9d9b2fc18ac +Subproject commit ff1e6543845e3b8c53a5f8b240bc38faffb1b3bf diff --git a/Penumbra.Api b/Penumbra.Api index e4934ccc..1750c41b 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit e4934ccca0379f22dadf989ab2d34f30b3c5c7ea +Subproject commit 1750c41b53e1000c99a7fb9d8a0f082aef639a41 diff --git a/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj b/Penumbra.CrashHandler/Penumbra.CrashHandler.csproj index 4c864d39..e07bb745 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 3d4d8510..0e973ed6 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 3d4d8510f832dfd95d7069b86e6b3da4ec612558 +Subproject commit 0e973ed6eace6afd31cd298f8c58f76fa8d5ef60 diff --git a/Penumbra.String b/Penumbra.String index 0315144a..9bd016fb 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 0315144ab5614c11911e2a4dddf436fb18c5d7e3 +Subproject commit 9bd016fbef5fb2de467dd42165267fdd93cd9592 diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 43f853f3..f9e33219 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 32032282..975c5bb3 100644 --- a/Penumbra/Penumbra.json +++ b/Penumbra/Penumbra.json @@ -8,7 +8,7 @@ "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "Tags": [ "modding" ], - "DalamudApiLevel": 13, + "DalamudApiLevel": 14, "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, diff --git a/repo.json b/repo.json index 7ddffd7c..5b780560 100644 --- a/repo.json +++ b/repo.json @@ -9,8 +9,8 @@ "TestingAssemblyVersion": "1.5.1.8", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", - "DalamudApiLevel": 13, - "TestingDalamudApiLevel": 13, + "DalamudApiLevel": 14, + "TestingDalamudApiLevel": 14, "IsHide": "False", "IsTestingExclusive": "False", "DownloadCount": 0, From fb299d71f0921c6efad318d0b648bad1c0076e61 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:54:09 +0100 Subject: [PATCH 853/865] Remove unimplemented ipc. --- Penumbra/Api/Api/UiApi.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index 6a293678..6fb116f3 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -85,12 +85,6 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - public PenumbraApiEc RegisterSettingsSection(Action draw) - => throw new NotImplementedException(); - - public PenumbraApiEc UnregisterSettingsSection(Action draw) - => throw new NotImplementedException(); - private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) From 37f30443767b9367970e16b1aba03d9608885592 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 00:56:50 +0100 Subject: [PATCH 854/865] Update dotnet. --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/test_release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7901a653..85ea0953 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '9.x.x' + dotnet-version: '10.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 377919b2..e4a17130 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '9.x.x' + dotnet-version: '10.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 2bece720..8af4a8c8 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '9.x.x' + dotnet-version: '10.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From 59fec5db822240213457b8e0b92566a9fad0ab89 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 01:05:50 +0100 Subject: [PATCH 855/865] Needs both versions for now due to flatsharp? --- .github/workflows/build.yml | 4 +++- .github/workflows/release.yml | 4 +++- .github/workflows/test_release.yml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85ea0953..26b1219d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '10.x.x' + dotnet-version: | + '10.x.x' + '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4a17130..a4442c14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '10.x.x' + dotnet-version: | + '10.x.x' + '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 8af4a8c8..914eb136 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -15,7 +15,9 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '10.x.x' + dotnet-version: | + '10.x.x' + '9.x.x' - name: Restore dependencies run: dotnet restore - name: Download Dalamud From 953f243caf7f03a58e67ae75dae3962e89cd55f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 01:08:18 +0100 Subject: [PATCH 856/865] . --- .github/workflows/build.yml | 8 ++++---- .github/workflows/release.yml | 8 ++++---- .github/workflows/test_release.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26b1219d..1a61439e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,15 +10,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - '10.x.x' - '9.x.x' + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4442c14..c72b4800 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,15 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - '10.x.x' - '9.x.x' + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 914eb136..90a8b176 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -9,15 +9,15 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - '10.x.x' - '9.x.x' + 10.x.x + 9.x.x - name: Restore dependencies run: dotnet restore - name: Download Dalamud From deb3686df5cf1f30365d70b3a2f382863136e953 Mon Sep 17 00:00:00 2001 From: Actions User Date: Fri, 19 Dec 2025 00:12:44 +0000 Subject: [PATCH 857/865] [CI] Updating repo.json for 1.5.1.9 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 5b780560..611de678 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.8", - "TestingAssemblyVersion": "1.5.1.8", + "AssemblyVersion": "1.5.1.9", + "TestingAssemblyVersion": "1.5.1.9", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.8/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 9cf7030f87142f6ae446b64601f56f9e849e67ca Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 19 Dec 2025 01:13:02 +0100 Subject: [PATCH 858/865] ... --- .github/workflows/test_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 90a8b176..c6b4e459 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -9,7 +9,7 @@ jobs: build: runs-on: windows-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v5 with: submodules: recursive - name: Setup .NET From eff3784a85c6fb498ba7f9655d6b9d26b524953e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Dec 2025 15:34:08 +0100 Subject: [PATCH 859/865] Fix multi-release bug in texturearrayslicer. --- Penumbra/Interop/Services/TextureArraySlicer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs index 3cd57a33..a3db4d04 100644 --- a/Penumbra/Interop/Services/TextureArraySlicer.cs +++ b/Penumbra/Interop/Services/TextureArraySlicer.cs @@ -18,7 +18,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable /// Caching this across frames will cause a crash to desktop. public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex) { - if (texture == null) + if (texture is null) throw new ArgumentNullException(nameof(texture)); if (sliceIndex >= texture->ArraySize) throw new ArgumentOutOfRangeException(nameof(sliceIndex), @@ -74,9 +74,6 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable { ID3D11ShaderResourceView* slicedSrv = null; Marshal.ThrowExceptionForHR(device->CreateShaderResourceView(resource, &description, &slicedSrv)); - resource->Release(); - device->Release(); - state = new SliceState(slicedSrv); _activeSlices.Add(((nint)texture, sliceIndex), state); return new ImTextureID((nint)state.ShaderResourceView); From 9aa566f521d0375959ceeffee2e9fcbce660c999 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Dec 2025 15:34:50 +0100 Subject: [PATCH 860/865] Fix typo in new IPC providers. --- Penumbra.Api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra.Api b/Penumbra.Api index 1750c41b..52a3216a 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 1750c41b53e1000c99a7fb9d8a0f082aef639a41 +Subproject commit 52a3216a525592205198303df2844435e382cf87 From 069323cfb8220b893df878b5067ee9d87656ab9a Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Dec 2025 14:38:27 +0000 Subject: [PATCH 861/865] [CI] Updating repo.json for 1.5.1.11 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 611de678..f337e8ff 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.9", - "TestingAssemblyVersion": "1.5.1.9", + "AssemblyVersion": "1.5.1.11", + "TestingAssemblyVersion": "1.5.1.11", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.9/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 73f02851a64e2a0ea9447173a7981d098a38bac1 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 20 Dec 2025 21:51:34 +0100 Subject: [PATCH 862/865] Cherry pick API support for other block compression types from Luna branch. --- Penumbra/Api/Api/EditingApi.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Penumbra/Api/Api/EditingApi.cs b/Penumbra/Api/Api/EditingApi.cs index e50b7a1b..5a1fc347 100644 --- a/Penumbra/Api/Api/EditingApi.cs +++ b/Penumbra/Api/Api/EditingApi.cs @@ -19,6 +19,12 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, inputFile, outputFile), TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, inputFile, outputFile), TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, inputFile, outputFile), + TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, inputFile, outputFile), + TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, inputFile, outputFile), + TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, inputFile, outputFile), + TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, inputFile, outputFile), + TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, inputFile, outputFile), + TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, inputFile, outputFile), _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), }; @@ -36,6 +42,12 @@ public class EditingApi(TextureManager textureManager) : IPenumbraApiEditing, IA TextureType.Bc3Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC3, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.Bc7Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), TextureType.Bc7Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC7, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc1Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc1Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC1, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc4Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc4Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC4, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc5Tex => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, true, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), + TextureType.Bc5Dds => textureManager.SaveAs(CombinedTexture.TextureSaveType.BC5, mipMaps, false, new BaseImage(), outputFile, rgbaData, width, rgbaData.Length / 4 / width), _ => Task.FromException(new Exception($"Invalid input value {textureType}.")), }; // @formatter:on From 6ba735eefba180538190842973604e8b0a592d0d Mon Sep 17 00:00:00 2001 From: Actions User Date: Sat, 20 Dec 2025 20:53:36 +0000 Subject: [PATCH 863/865] [CI] Updating repo.json for 1.5.1.12 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index f337e8ff..583e5e52 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.11", - "TestingAssemblyVersion": "1.5.1.11", + "AssemblyVersion": "1.5.1.12", + "TestingAssemblyVersion": "1.5.1.12", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.11/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ] From 13500264b7f046cacbddae41df704089be7e7908 Mon Sep 17 00:00:00 2001 From: Marc-Aurel Zent Date: Mon, 22 Dec 2025 14:41:32 +0100 Subject: [PATCH 864/865] Use iced to create AsmHooks in PapRewriter. --- .../Hooks/ResourceLoading/PapRewriter.cs | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs index caf43d08..ff794d81 100644 --- a/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs +++ b/Penumbra/Interop/Hooks/ResourceLoading/PapRewriter.cs @@ -1,6 +1,7 @@ using System.Text.Unicode; using Dalamud.Hooking; using Iced.Intel; +using static Iced.Intel.AssemblerRegisters; using OtterGui.Extensions; using Penumbra.String.Classes; using Swan; @@ -46,36 +47,32 @@ public sealed class PapRewriter(PeSigScanner sigScanner, PapRewriter.PapResource stackAccesses.RemoveAll(instr => instr.IP == hp.IP); var detourPointer = Marshal.GetFunctionPointerForDelegate(papResourceHandler); - var targetRegister = hookPoint.Op0Register.ToString().ToLower(); + var targetRegister = GetRegister64(hookPoint.Op0Register); var hookAddress = new IntPtr((long)detourPoint.IP); var caveAllocation = NativeAllocCave(16); - var hook = new AsmHook( - hookAddress, - [ - "use64", - $"mov {targetRegister}, 0x{stringAllocation:x8}", // Move our char *path into the relevant register (rdx) + var assembler = new Assembler(64); + assembler.mov(targetRegister, stringAllocation); // Move our char *path into the relevant register (rdx) - // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves - // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call - // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh - $"mov r9, 0x{caveAllocation:x8}", - "mov [r9], rcx", - "mov [r9+0x8], rdx", + // After this asm stub, we have a call to Crc32(); since r9 is a volatile, unused register, we can use it ourselves + // We're essentially storing the original 2 arguments ('this', 'path'), in case they get mangled in our call + // We technically don't need to save rdx ('path'), since it'll be stringLoc, but eh + assembler.mov(r9, caveAllocation); + assembler.mov(__qword_ptr[r9], rcx); + assembler.mov(__qword_ptr[r9 + 8], rdx); - // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway - $"mov rax, 0x{detourPointer:x8}", // Get a pointer to our detour in place - "call rax", // Call detour + // We can use 'rax' here too since it's also volatile, and it'll be overwritten by Crc32()'s return anyway + assembler.mov(rax, detourPointer); + assembler.call(rax); - // Do the reverse process and retrieve the stored stuff - $"mov r9, 0x{caveAllocation:x8}", - "mov rcx, [r9]", - "mov rdx, [r9+0x8]", + // Do the reverse process and retrieve the stored stuff + assembler.mov(r9, caveAllocation); + assembler.mov(rcx, __qword_ptr[r9]); + assembler.mov(rdx, __qword_ptr[r9 + 8]); - // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call - "mov r8, rax", - ], $"{name}.PapRedirection" - ); + // Plop 'rax' (our return value, the path size) into r8, so it's the third argument for the subsequent Crc32() call + assembler.mov(r8, rax); + var hook = new AsmHook(hookAddress, AssembleToBytes(assembler), $"{name}.PapRedirection"); _hooks.Add(hookAddress, hook); hook.Enable(); @@ -95,19 +92,45 @@ public sealed class PapRewriter(PeSigScanner sigScanner, PapRewriter.PapResource if (_hooks.ContainsKey(hookAddress)) continue; - var targetRegister = stackAccess.Op0Register.ToString().ToLower(); - var hook = new AsmHook( - hookAddress, - [ - "use64", - $"mov {targetRegister}, 0x{stringAllocation:x8}", - ], $"{name}.PapStackAccess[{index}]" - ); + var targetRegister = GetRegister64(stackAccess.Op0Register); + var assembler = new Assembler(64); + assembler.mov(targetRegister, stringAllocation); + var hook = new AsmHook(hookAddress, AssembleToBytes(assembler), $"{name}.PapStackAccess[{index}]"); _hooks.Add(hookAddress, hook); hook.Enable(); } } + + private static AssemblerRegister64 GetRegister64(Register reg) + => reg switch + { + Register.RAX => rax, + Register.RCX => rcx, + Register.RDX => rdx, + Register.RBX => rbx, + Register.RSP => rsp, + Register.RBP => rbp, + Register.RSI => rsi, + Register.RDI => rdi, + Register.R8 => r8, + Register.R9 => r9, + Register.R10 => r10, + Register.R11 => r11, + Register.R12 => r12, + Register.R13 => r13, + Register.R14 => r14, + Register.R15 => r15, + _ => throw new ArgumentOutOfRangeException(nameof(reg), reg, "Unsupported register."), + }; + + private static byte[] AssembleToBytes(Assembler assembler) + { + using var stream = new MemoryStream(); + var writer = new StreamCodeWriter(stream); + assembler.Assemble(writer, 0); + return stream.ToArray(); + } private static IEnumerable ScanStackAccesses(IEnumerable instructions, Instruction hookPoint) { From eec8ee709411f18704b95b543fb4966b525b0428 Mon Sep 17 00:00:00 2001 From: Actions User Date: Tue, 27 Jan 2026 15:30:23 +0000 Subject: [PATCH 865/865] [CI] Updating repo.json for 1.5.1.13 --- repo.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/repo.json b/repo.json index 583e5e52..22a682ee 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.12", - "TestingAssemblyVersion": "1.5.1.12", + "AssemblyVersion": "1.5.1.13", + "TestingAssemblyVersion": "1.5.1.13", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 14, @@ -18,9 +18,9 @@ "LoadPriority": 69420, "LoadRequiredState": 2, "LoadSync": true, - "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", - "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.12/Penumbra.zip", + "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.13/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.13/Penumbra.zip", + "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.13/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" } ]