mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Make human.pbd moddable
This commit is contained in:
parent
12532dee28
commit
efdd5a824b
11 changed files with 202 additions and 12 deletions
|
|
@ -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<object> Conflicts, bool HasPriority,
|
|||
/// </summary>
|
||||
public class CollectionCache : IDisposable
|
||||
{
|
||||
private readonly CollectionCacheManager _manager;
|
||||
private readonly ModCollection _collection;
|
||||
public readonly CollectionModData ModData = new();
|
||||
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly MetaCache Meta;
|
||||
public readonly Dictionary<IMod, SingleArray<ModConflicts>> ConflictDict = [];
|
||||
private readonly CollectionCacheManager _manager;
|
||||
private readonly ModCollection _collection;
|
||||
public readonly CollectionModData ModData = new();
|
||||
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = [];
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly ConcurrentDictionary<Utf8GamePath, SafeResourceHandle> LoadedResources = new();
|
||||
public readonly MetaCache Meta;
|
||||
public readonly Dictionary<IMod, SingleArray<ModConflicts>> 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));
|
||||
|
||||
/// <summary> Invalidates caches subsequently to a resolved file being modified. </summary>
|
||||
private void InvalidateResolvedFile(Utf8GamePath path)
|
||||
{
|
||||
if (LoadedResources.Remove(path, out var handle))
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary> The function to use to resolve a given path. </summary>
|
||||
public Func<Utf8GamePath, ResourceCategory, ResourceType, (FullPath?, ResolveData)> ResolvePath = null!;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ResourceHandlePrototype>(
|
||||
(nint)FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.ResourceHandle.MemberFunctionPointers.IncRef,
|
||||
(nint)CSResourceHandle.MemberFunctionPointers.IncRef,
|
||||
ResourceHandleIncRefDetour);
|
||||
_incRefHook.Enable();
|
||||
_decRefHook = interop.HookFromAddress<ResourceHandleDecRefPrototype>(
|
||||
(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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
95
Penumbra/Interop/Services/PreBoneDeformerReplacer.cs
Normal file
95
Penumbra/Interop/Services/PreBoneDeformerReplacer.cs
Normal file
|
|
@ -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<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)
|
||||
{
|
||||
interop.InitializeFromAttributes(this);
|
||||
_utility = utility;
|
||||
_collectionResolver = collectionResolver;
|
||||
_resourceLoader = resourceLoader;
|
||||
_framework = framework;
|
||||
_humanSetupScalingHook = interop.HookFromAddress<CharacterBaseSetupScalingDelegate>(_humanVTable[57], SetupScaling);
|
||||
_humanCreateDeformerHook = interop.HookFromAddress<CharacterBaseCreateDeformerDelegate>(_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ public class Penumbra : IDalamudPlugin
|
|||
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
|
||||
_collectionManager.Caches.CreateNecessaryCaches();
|
||||
_services.GetService<PathResolver>();
|
||||
_services.GetService<PreBoneDeformerReplacer>();
|
||||
_services.GetService<ShaderReplacementFixer>();
|
||||
|
||||
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ public static class StaticServiceManager
|
|||
.AddSingleton<ResourceWatcher>()
|
||||
.AddSingleton<ResourceTreeFactory>()
|
||||
.AddSingleton<MetaFileManager>()
|
||||
.AddSingleton<PreBoneDeformerReplacer>()
|
||||
.AddSingleton<ShaderReplacementFixer>();
|
||||
|
||||
private static ServiceManager AddResolvers(this ServiceManager services)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue