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)