diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index 9a0e525b..6b2b688b 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -18,13 +18,14 @@ 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 CustomResourceCache CustomResources; public readonly MetaCache Meta; public readonly Dictionary> ConflictDict = []; @@ -37,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; @@ -54,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) @@ -72,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; @@ -100,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); @@ -108,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) @@ -148,17 +154,20 @@ public class CollectionCache : IDisposable if (fullPath.FullName.Length > 0) { ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, Mod.ForcedFiles); } else { + 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)); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); } } @@ -181,6 +190,7 @@ public class CollectionCache : IDisposable { if (ResolvedFiles.Remove(path, out var mp)) { + 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}."); @@ -295,6 +305,7 @@ public class CollectionCache : IDisposable if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) { ModData.AddPath(mod, path); + CustomResources.Invalidate(path); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); return; } @@ -309,13 +320,14 @@ public class CollectionCache : IDisposable ModData.RemovePath(modPath.Mod, path); ResolvedFiles[path] = new ModPath(mod, file); ModData.AddPath(mod, 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 8ccdfa80..6c2c83b3 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,15 @@ 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; + } + /// 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..dac86e44 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.CustomResources.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..9f553257 --- /dev/null +++ b/Penumbra/Interop/Services/PreBoneDeformerReplacer.cs @@ -0,0 +1,97 @@ +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using OtterGui.Services; +using Penumbra.Api.Enums; +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, IRequiredService +{ + public static readonly Utf8GamePath PreBoneDeformerPath = + Utf8GamePath.FromSpan("chara/xls/boneDeformer/human.pbd"u8, 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); + + 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, 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(); + } + + public void Dispose() + { + _humanCreateDeformerHook.Dispose(); + _humanSetupScalingHook.Dispose(); + } + + private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) + { + var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); + 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"); + + 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/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/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..b76780c0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -77,7 +77,6 @@ public class Penumbra : IDalamudPlugin _services.GetService(); // Initialize because not required anywhere else. _collectionManager.Caches.CreateNecessaryCaches(); _services.GetService(); - _services.GetService(); _services.GetService(); // Initialize before Interface. diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 66c90e84..9e6071b4 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -131,8 +131,7 @@ public static class StaticServiceManager => services.AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddResolvers(this ServiceManager services) => services.AddSingleton()