Merge branch 'pbd-modding'

This commit is contained in:
Ottermandias 2024-03-28 18:14:36 +01:00
commit a31bdb66c8
14 changed files with 254 additions and 41 deletions

View file

@ -18,13 +18,14 @@ public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority,
/// The Cache contains all required temporary data to use a collection. /// The Cache contains all required temporary data to use a collection.
/// It will only be setup if a collection gets activated in any way. /// It will only be setup if a collection gets activated in any way.
/// </summary> /// </summary>
public class CollectionCache : IDisposable public sealed class CollectionCache : IDisposable
{ {
private readonly CollectionCacheManager _manager; private readonly CollectionCacheManager _manager;
private readonly ModCollection _collection; private readonly ModCollection _collection;
public readonly CollectionModData ModData = new(); public readonly CollectionModData ModData = new();
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = []; private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new(); public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly CustomResourceCache CustomResources;
public readonly MetaCache Meta; public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = []; public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
@ -37,7 +38,7 @@ public class CollectionCache : IDisposable
=> ConflictDict.Values; => ConflictDict.Values;
public SingleArray<ModConflicts> Conflicts(IMod mod) public SingleArray<ModConflicts> Conflicts(IMod mod)
=> ConflictDict.TryGetValue(mod, out SingleArray<ModConflicts> c) ? c : new SingleArray<ModConflicts>(); => ConflictDict.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
private int _changedItemsSaveCounter = -1; private int _changedItemsSaveCounter = -1;
@ -54,16 +55,21 @@ public class CollectionCache : IDisposable
// The cache reacts through events on its collection changing. // The cache reacts through events on its collection changing.
public CollectionCache(CollectionCacheManager manager, ModCollection collection) public CollectionCache(CollectionCacheManager manager, ModCollection collection)
{ {
_manager = manager; _manager = manager;
_collection = collection; _collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection); Meta = new MetaCache(manager.MetaFileManager, _collection);
CustomResources = new CustomResourceCache(manager.ResourceLoader);
} }
public void Dispose() public void Dispose()
=> Meta.Dispose(); {
Meta.Dispose();
CustomResources.Dispose();
GC.SuppressFinalize(this);
}
~CollectionCache() ~CollectionCache()
=> Meta.Dispose(); => Dispose();
// Resolve a given game path according to this collection. // Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath) public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
@ -72,7 +78,7 @@ public class CollectionCache : IDisposable
return null; return null;
if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.Path.IsRooted && !candidate.Path.Exists) || candidate.Path is { IsRooted: true, Exists: false })
return null; return null;
return candidate.Path; return candidate.Path;
@ -100,7 +106,7 @@ public class CollectionCache : IDisposable
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths) public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths)
{ {
if (fullPaths.Count == 0) if (fullPaths.Count == 0)
return Array.Empty<HashSet<Utf8GamePath>>(); return [];
var ret = new HashSet<Utf8GamePath>[fullPaths.Count]; var ret = new HashSet<Utf8GamePath>[fullPaths.Count];
var dict = new Dictionary<FullPath, int>(fullPaths.Count); var dict = new Dictionary<FullPath, int>(fullPaths.Count);
@ -108,8 +114,8 @@ public class CollectionCache : IDisposable
{ {
dict[new FullPath(path)] = idx; dict[new FullPath(path)] = idx;
ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8) ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8)
? new HashSet<Utf8GamePath> { utf8 } ? [utf8]
: new HashSet<Utf8GamePath>(); : [];
} }
foreach (var (game, full) in ResolvedFiles) foreach (var (game, full) in ResolvedFiles)
@ -148,17 +154,20 @@ public class CollectionCache : IDisposable
if (fullPath.FullName.Length > 0) if (fullPath.FullName.Length > 0)
{ {
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path, InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, fullPath, modPath.Path,
Mod.ForcedFiles); Mod.ForcedFiles);
} }
else else
{ {
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Removed, path, FullPath.Empty, modPath.Path, null);
} }
} }
else if (fullPath.FullName.Length > 0) else if (fullPath.FullName.Length > 0)
{ {
ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath)); ResolvedFiles.TryAdd(path, new ModPath(Mod.ForcedFiles, fullPath));
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, fullPath, FullPath.Empty, Mod.ForcedFiles); 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)) if (ResolvedFiles.Remove(path, out var mp))
{ {
CustomResources.Invalidate(path);
if (mp.Mod != mod) if (mp.Mod != mod)
Penumbra.Log.Warning( Penumbra.Log.Warning(
$"Invalid mod state, removing {mod.Name} and associated file {path} returned current mod {mp.Mod.Name}."); $"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))) if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
{ {
ModData.AddPath(mod, path); ModData.AddPath(mod, path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Added, path, file, FullPath.Empty, mod);
return; return;
} }
@ -309,13 +320,14 @@ public class CollectionCache : IDisposable
ModData.RemovePath(modPath.Mod, path); ModData.RemovePath(modPath.Mod, path);
ResolvedFiles[path] = new ModPath(mod, file); ResolvedFiles[path] = new ModPath(mod, file);
ModData.AddPath(mod, path); ModData.AddPath(mod, path);
CustomResources.Invalidate(path);
InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod); InvokeResolvedFileChange(_collection, ResolvedFileChanged.Type.Replaced, path, file, modPath.Path, mod);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Penumbra.Log.Error( 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}");
} }
} }

View file

@ -4,6 +4,7 @@ using Penumbra.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
@ -21,8 +22,8 @@ public class CollectionCacheManager : IDisposable
private readonly CollectionStorage _storage; private readonly CollectionStorage _storage;
private readonly ActiveCollections _active; private readonly ActiveCollections _active;
internal readonly ResolvedFileChanged ResolvedFileChanged; internal readonly ResolvedFileChanged ResolvedFileChanged;
internal readonly MetaFileManager MetaFileManager;
internal readonly MetaFileManager MetaFileManager; internal readonly ResourceLoader ResourceLoader;
private readonly ConcurrentQueue<CollectionCache.ChangeData> _changeQueue = new(); private readonly ConcurrentQueue<CollectionCache.ChangeData> _changeQueue = new();
@ -35,7 +36,7 @@ public class CollectionCacheManager : IDisposable
=> _storage.Where(c => c.HasCache); => _storage.Where(c => c.HasCache);
public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator, TempModManager tempMods, ModStorage modStorage, 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; _framework = framework;
_communicator = communicator; _communicator = communicator;
@ -44,6 +45,7 @@ public class CollectionCacheManager : IDisposable
MetaFileManager = metaFileManager; MetaFileManager = metaFileManager;
_active = active; _active = active;
_storage = storage; _storage = storage;
ResourceLoader = resourceLoader;
ResolvedFileChanged = _communicator.ResolvedFileChanged; ResolvedFileChanged = _communicator.ResolvedFileChanged;
if (!_active.Individuals.IsLoaded) if (!_active.Individuals.IsLoaded)

View file

@ -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;
/// <summary> A cache for resources owned by a collection. </summary>
public sealed class CustomResourceCache(ResourceLoader loader)
: ConcurrentDictionary<Utf8GamePath, SafeResourceHandle>, IDisposable
{
/// <summary> Invalidate an existing resource by clearing it from the cache and disposing it. </summary>
public void Invalidate(Utf8GamePath path)
{
if (TryRemove(path, out var handle))
handle.Dispose();
}
public void Dispose()
{
foreach (var handle in Values)
handle.Dispose();
Clear();
}
/// <summary> Get the requested resource either from the cached resource, or load a new one if it does not exist. </summary>
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;
}
/// <summary> Get a cloned cached resource if it exists. </summary>
private bool TryGetClonedValue(Utf8GamePath path, [NotNullWhen(true)] out SafeResourceHandle? handle)
{
if (!TryGetValue(path, out handle))
return false;
handle = handle.Clone();
return true;
}
}

View file

@ -1,6 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Interop.SafeHandles;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -39,6 +40,15 @@ public unsafe class ResourceLoader : IDisposable
return ret; return ret;
} }
/// <summary> Load a resource for a given path and a specific collection. </summary>
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;
}
/// <summary> The function to use to resolve a given path. </summary> /// <summary> The function to use to resolve a given path. </summary>
public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!; public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!;

View file

@ -4,10 +4,12 @@ using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.SafeHandles;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util; using Penumbra.Util;
using CSResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle;
namespace Penumbra.Interop.ResourceLoading; namespace Penumbra.Interop.ResourceLoading;
@ -25,11 +27,11 @@ public unsafe class ResourceService : IDisposable
_getResourceAsyncHook.Enable(); _getResourceAsyncHook.Enable();
_resourceHandleDestructorHook.Enable(); _resourceHandleDestructorHook.Enable();
_incRefHook = interop.HookFromAddress<ResourceHandlePrototype>( _incRefHook = interop.HookFromAddress<ResourceHandlePrototype>(
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef, (nint)CSResourceHandle.MemberFunctionPointers.IncRef,
ResourceHandleIncRefDetour); ResourceHandleIncRefDetour);
_incRefHook.Enable(); _incRefHook.Enable();
_decRefHook = interop.HookFromAddress<ResourceHandleDecRefPrototype>( _decRefHook = interop.HookFromAddress<ResourceHandleDecRefPrototype>(
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.DecRef, (nint)CSResourceHandle.MemberFunctionPointers.DecRef,
ResourceHandleDecRefDetour); ResourceHandleDecRefDetour);
_decRefHook.Enable(); _decRefHook.Enable();
} }
@ -41,6 +43,9 @@ public unsafe class ResourceService : IDisposable
&category, &type, &hash, path.Path, null, false); &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() public void Dispose()
{ {
_getResourceSyncHook.Dispose(); _getResourceSyncHook.Dispose();

View file

@ -9,6 +9,7 @@ using Penumbra.Collections;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.UI; using Penumbra.UI;
@ -144,6 +145,14 @@ internal unsafe partial record ResolveContext(
return GetOrCreateNode(ResourceType.Imc, 0, imc, path); 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) public ResourceNode? CreateNodeFromTex(TextureResourceHandle* tex, string gamePath)
{ {
if (tex == null) if (tex == null)

View file

@ -1,4 +1,3 @@
using Dalamud.Game.ClientState.Objects.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
@ -6,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Services;
using Penumbra.UI; using Penumbra.UI;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
@ -155,6 +155,22 @@ public class ResourceTree
{ {
var genericContext = globalContext.CreateContext(&human->CharacterBase); 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 decalId = (byte)(human->Customize[(int)CustomizeIndex.Facepaint] & 0x7F);
var decalPath = decalId != 0 var decalPath = decalId != 0
? GamePaths.Human.Decal.FaceDecalPath(decalId) ? GamePaths.Human.Decal.FaceDecalPath(decalId)

View file

@ -1,8 +1,8 @@
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
namespace Penumbra.Interop.SafeHandles; namespace Penumbra.Interop.SafeHandles;
public unsafe class SafeResourceHandle : SafeHandle public unsafe class SafeResourceHandle : SafeHandle, ICloneable
{ {
public ResourceHandle* ResourceHandle public ResourceHandle* ResourceHandle
=> (ResourceHandle*)handle; => (ResourceHandle*)handle;
@ -21,6 +21,12 @@ public unsafe class SafeResourceHandle : SafeHandle
SetHandle((nint)handle); SetHandle((nint)handle);
} }
public SafeResourceHandle Clone()
=> new(ResourceHandle, true);
object ICloneable.Clone()
=> Clone();
public static SafeResourceHandle CreateInvalid() public static SafeResourceHandle CreateInvalid()
=> new(null, false); => new(null, false);

View file

@ -28,6 +28,7 @@ public unsafe class CharacterUtility : IDisposable
public bool Ready { get; private set; } public bool Ready { get; private set; }
public event Action LoadingFinished; public event Action LoadingFinished;
public nint DefaultHumanPbdResource { get; private set; }
public nint DefaultTransparentResource { get; private set; } public nint DefaultTransparentResource { get; private set; }
public nint DefaultDecalResource { get; private set; } public nint DefaultDecalResource { get; private set; }
public nint DefaultSkinShpkResource { get; private set; } public nint DefaultSkinShpkResource { get; private set; }
@ -88,6 +89,12 @@ public unsafe class CharacterUtility : IDisposable
anyMissing |= !_lists[i].Ready; anyMissing |= !_lists[i].Ready;
} }
if (DefaultHumanPbdResource == nint.Zero)
{
DefaultHumanPbdResource = (nint)Address->HumanPbdResource;
anyMissing |= DefaultHumanPbdResource == nint.Zero;
}
if (DefaultTransparentResource == nint.Zero) if (DefaultTransparentResource == nint.Zero)
{ {
DefaultTransparentResource = (nint)Address->TransparentTexResource; DefaultTransparentResource = (nint)Address->TransparentTexResource;
@ -151,6 +158,7 @@ public unsafe class CharacterUtility : IDisposable
foreach (var list in _lists) foreach (var list in _lists)
list.Dispose(); list.Dispose();
Address->HumanPbdResource = (ResourceHandle*)DefaultHumanPbdResource;
Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource; Address->TransparentTexResource = (TextureResourceHandle*)DefaultTransparentResource;
Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource; Address->DecalTexResource = (TextureResourceHandle*)DefaultDecalResource;
Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource; Address->SkinShpkResource = (ResourceHandle*)DefaultSkinShpkResource;

View file

@ -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<CharacterBaseSetupScalingDelegate> _humanSetupScalingHook;
private readonly Hook<CharacterBaseCreateDeformerDelegate> _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<CharacterBaseSetupScalingDelegate>(vTables.HumanVTable[57], SetupScaling);
_humanCreateDeformerHook = interop.HookFromAddress<CharacterBaseCreateDeformerDelegate>(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;
}
}
}

View file

@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Interop.Hooks.Resources; using Penumbra.Interop.Hooks.Resources;
@ -13,7 +14,7 @@ using CSModelRenderer = FFXIVClientStructs.FFXIV.Client.Graphics.Render.ModelRen
namespace Penumbra.Interop.Services; namespace Penumbra.Interop.Services;
public sealed unsafe class ShaderReplacementFixer : IDisposable public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredService
{ {
public static ReadOnlySpan<byte> SkinShpkName public static ReadOnlySpan<byte> SkinShpkName
=> "skin.shpk"u8; => "skin.shpk"u8;
@ -21,11 +22,10 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
public static ReadOnlySpan<byte> CharacterGlassShpkName public static ReadOnlySpan<byte> CharacterGlassShpkName
=> "characterglass.shpk"u8; => "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 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<CharacterBaseOnRenderMaterialDelegate> _humanOnRenderMaterialHook; private readonly Hook<CharacterBaseOnRenderMaterialDelegate> _humanOnRenderMaterialHook;
@ -59,14 +59,15 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
=> _moddedCharacterGlassShpkCount; => _moddedCharacterGlassShpkCount;
public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer, public ShaderReplacementFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, ModelRenderer modelRenderer,
CommunicatorService communicator, IGameInteropProvider interop) CommunicatorService communicator, IGameInteropProvider interop, CharacterBaseVTables vTables)
{ {
interop.InitializeFromAttributes(this); interop.InitializeFromAttributes(this);
_resourceHandleDestructor = resourceHandleDestructor; _resourceHandleDestructor = resourceHandleDestructor;
_utility = utility; _utility = utility;
_modelRenderer = modelRenderer; _modelRenderer = modelRenderer;
_communicator = communicator; _communicator = communicator;
_humanOnRenderMaterialHook = interop.HookFromAddress<CharacterBaseOnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial); _humanOnRenderMaterialHook =
interop.HookFromAddress<CharacterBaseOnRenderMaterialDelegate>(vTables.HumanVTable[62], OnRenderHumanMaterial);
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer); _communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.ShaderReplacementFixer);
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer); _resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.ShaderReplacementFixer);
_humanOnRenderMaterialHook.Enable(); _humanOnRenderMaterialHook.Enable();
@ -82,7 +83,7 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
_moddedCharacterGlassShpkMaterials.Clear(); _moddedCharacterGlassShpkMaterials.Clear();
_moddedSkinShpkMaterials.Clear(); _moddedSkinShpkMaterials.Clear();
_moddedCharacterGlassShpkCount = 0; _moddedCharacterGlassShpkCount = 0;
_moddedSkinShpkCount = 0; _moddedSkinShpkCount = 0;
} }
public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas() public (ulong Skin, ulong CharacterGlass) GetAndResetSlowPathCallDeltas()
@ -106,16 +107,12 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable
var shpkName = mtrl->ShpkNameSpan; var shpkName = mtrl->ShpkNameSpan;
if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource) if (SkinShpkName.SequenceEqual(shpkName) && (nint)shpk != _utility.DefaultSkinShpkResource)
{
if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle)) if (_moddedSkinShpkMaterials.TryAdd(mtrlResourceHandle))
Interlocked.Increment(ref _moddedSkinShpkCount); Interlocked.Increment(ref _moddedSkinShpkCount);
}
if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage) if (CharacterGlassShpkName.SequenceEqual(shpkName) && shpk != _modelRenderer.DefaultCharacterGlassShaderPackage)
{
if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle)) if (_moddedCharacterGlassShpkMaterials.TryAdd(mtrlResourceHandle))
Interlocked.Increment(ref _moddedCharacterGlassShpkCount); Interlocked.Increment(ref _moddedCharacterGlassShpkCount);
}
} }
private void OnResourceHandleDestructor(Structs.ResourceHandle* handle) 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 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 || _moddedCharacterGlassShpkCount == 0)
return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex); return _modelRendererOnRenderMaterialHook.Original(modelRenderer, outFlags, param, material, materialIndex);

View file

@ -5,6 +5,7 @@ namespace Penumbra.Interop.Structs;
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterUtilityData public unsafe struct CharacterUtilityData
{ {
public const int IndexHumanPbd = 63;
public const int IndexTransparentTex = 72; public const int IndexTransparentTex = 72;
public const int IndexDecalTex = 73; public const int IndexDecalTex = 73;
public const int IndexSkinShpk = 76; public const int IndexSkinShpk = 76;
@ -72,6 +73,9 @@ public unsafe struct CharacterUtilityData
public ResourceHandle* EqdpResource(GenderRace raceCode, bool accessory) public ResourceHandle* EqdpResource(GenderRace raceCode, bool accessory)
=> Resource((int)EqdpIdx(raceCode, accessory)); => Resource((int)EqdpIdx(raceCode, accessory));
[FieldOffset(8 + IndexHumanPbd * 8)]
public ResourceHandle* HumanPbdResource;
[FieldOffset(8 + (int)MetaIndex.HumanCmp * 8)] [FieldOffset(8 + (int)MetaIndex.HumanCmp * 8)]
public ResourceHandle* HumanCmpResource; public ResourceHandle* HumanCmpResource;

View file

@ -77,7 +77,6 @@ public class Penumbra : IDalamudPlugin
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else. _services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches(); _collectionManager.Caches.CreateNecessaryCaches();
_services.GetService<PathResolver>(); _services.GetService<PathResolver>();
_services.GetService<ShaderReplacementFixer>();
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface. _services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.

View file

@ -131,8 +131,7 @@ public static class StaticServiceManager
=> services.AddSingleton<ResourceLoader>() => services.AddSingleton<ResourceLoader>()
.AddSingleton<ResourceWatcher>() .AddSingleton<ResourceWatcher>()
.AddSingleton<ResourceTreeFactory>() .AddSingleton<ResourceTreeFactory>()
.AddSingleton<MetaFileManager>() .AddSingleton<MetaFileManager>();
.AddSingleton<ShaderReplacementFixer>();
private static ServiceManager AddResolvers(this ServiceManager services) private static ServiceManager AddResolvers(this ServiceManager services)
=> services.AddSingleton<CollectionResolver>() => services.AddSingleton<CollectionResolver>()