Path Resolver unfiddled and somewhat optimized.

This commit is contained in:
Ottermandias 2023-03-23 16:39:29 +01:00
parent b6d6993c9f
commit 7a6384bd22
24 changed files with 1976 additions and 2069 deletions

View file

@ -14,6 +14,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
@ -53,13 +54,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
add add
{ {
if (value == null)
return;
CheckInitialized(); CheckInitialized();
PathResolver.DrawObjectState.CreatingCharacterBase += value; _communicator.CreatingCharacterBase.Event += new Action<nint, string, nint, nint, nint>(value);
} }
remove remove
{ {
if (value == null)
return;
CheckInitialized(); CheckInitialized();
PathResolver.DrawObjectState.CreatingCharacterBase -= value; _communicator.CreatingCharacterBase.Event -= new Action<nint, string, nint, nint, nint>(value);
} }
} }
@ -67,13 +72,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
add add
{ {
if (value == null)
return;
CheckInitialized(); CheckInitialized();
PathResolver.DrawObjectState.CreatedCharacterBase += value; _communicator.CreatedCharacterBase.Event += new Action<nint, string, nint>(value);
} }
remove remove
{ {
if (value == null)
return;
CheckInitialized(); CheckInitialized();
PathResolver.DrawObjectState.CreatedCharacterBase -= value; _communicator.CreatedCharacterBase.Event -= new Action<nint, string, nint>(value);
} }
} }
@ -92,21 +101,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private TempCollectionManager _tempCollections; private TempCollectionManager _tempCollections;
private TempModManager _tempMods; private TempModManager _tempMods;
private ActorService _actors; private ActorService _actors;
private CollectionResolver _collectionResolver;
private CutsceneService _cutsceneService;
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader,
Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
TempModManager tempMods, ActorService actors) TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService)
{ {
_communicator = communicator; _communicator = communicator;
_penumbra = penumbra; _penumbra = penumbra;
_modManager = modManager; _modManager = modManager;
_resourceLoader = resourceLoader; _resourceLoader = resourceLoader;
_config = config; _config = config;
_collectionManager = collectionManager; _collectionManager = collectionManager;
_dalamud = dalamud; _dalamud = dalamud;
_tempCollections = tempCollections; _tempCollections = tempCollections;
_tempMods = tempMods; _tempMods = tempMods;
_actors = actors; _actors = actors;
_collectionResolver = collectionResolver;
_cutsceneService = cutsceneService;
_lumina = (Lumina.GameData?)_dalamud.GameData.GetType() _lumina = (Lumina.GameData?)_dalamud.GameData.GetType()
.GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic)
@ -144,6 +157,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_tempCollections = null!; _tempCollections = null!;
_tempMods = null!; _tempMods = null!;
_actors = null!; _actors = null!;
_collectionResolver = null!;
_cutsceneService = null!;
} }
public event ChangedItemClick? ChangedItemClicked; public event ChangedItemClick? ChangedItemClicked;
@ -157,7 +172,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData) ResolveData resolveData)
{ {
if (resolveData.AssociatedGameObject != IntPtr.Zero) if (resolveData.AssociatedGameObject != nint.Zero)
GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
manipulatedPath?.ToString() ?? originalPath.ToString()); manipulatedPath?.ToString() ?? originalPath.ToString());
} }
@ -275,7 +290,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public string ResolvePlayerPath(string path) public string ResolvePlayerPath(string path)
{ {
CheckInitialized(); CheckInitialized();
return ResolvePath(path, _modManager, PathResolver.PlayerCollection()); return ResolvePath(path, _modManager, _collectionResolver.PlayerCollection());
} }
// TODO: cleanup when incrementing API level // TODO: cleanup when incrementing API level
@ -336,7 +351,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
path, path,
}; };
var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); var ret = _collectionResolver.PlayerCollection().ReverseResolvePath(new FullPath(path));
return ret.Select(r => r.ToString()).ToArray(); return ret.Select(r => r.ToString()).ToArray();
} }
@ -349,7 +364,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
p, p,
}).ToArray()); }).ToArray());
var playerCollection = PathResolver.PlayerCollection(); var playerCollection = _collectionResolver.PlayerCollection();
var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray(); var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray();
var reverseResolved = playerCollection.ReverseResolvePaths(reverse); var reverseResolved = playerCollection.ReverseResolvePaths(reverse);
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
@ -525,17 +540,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi
: (_collectionManager.Default.Name, false); : (_collectionManager.Default.Name, false);
} }
public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject) public unsafe (nint, string) GetDrawObjectInfo(nint drawObject)
{ {
CheckInitialized(); CheckInitialized();
var (obj, collection) = PathResolver.IdentifyDrawObject(drawObject); var data = _collectionResolver.IdentifyCollection((DrawObject*) drawObject, true);
return (obj, collection.ModCollection.Name); return (data.AssociatedGameObject, data.ModCollection.Name);
} }
public int GetCutsceneParentIndex(int actorIdx) public int GetCutsceneParentIndex(int actorIdx)
{ {
CheckInitialized(); CheckInitialized();
return _penumbra!.PathResolver.CutsceneActor(actorIdx); return _cutsceneService.GetParentIndex(actorIdx);
} }
public IList<(string, string)> GetModList() public IList<(string, string)> GetModList()
@ -823,10 +838,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (!_actors.Valid) if (!_actors.Valid)
return PenumbraApiEc.SystemDisposed; return PenumbraApiEc.SystemDisposed;
if (actorIndex < 0 || actorIndex >= DalamudServices.SObjects.Length) if (actorIndex < 0 || actorIndex >= _dalamud.Objects.Length)
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
var identifier = _actors.AwaitedService.FromObject(DalamudServices.SObjects[actorIndex], false, false, true); var identifier = _actors.AwaitedService.FromObject(_dalamud.Objects[actorIndex], false, false, true);
if (!identifier.IsValid) if (!identifier.IsValid)
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
@ -926,7 +941,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public string GetPlayerMetaManipulations() public string GetPlayerMetaManipulations()
{ {
CheckInitialized(); CheckInitialized();
var collection = PathResolver.PlayerCollection(); var collection = _collectionResolver.PlayerCollection();
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>(); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>();
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
} }
@ -977,11 +992,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
{ {
collection = _collectionManager.Default; collection = _collectionManager.Default;
if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.SObjects.Length) if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length)
return false; return false;
var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx);
var data = PathResolver.IdentifyCollection(ptr, false); var data = _collectionResolver.IdentifyCollection(ptr, false);
if (data.Valid) if (data.Valid)
collection = data.ModCollection; collection = data.ModCollection;
@ -994,7 +1009,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid) if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid)
return ActorIdentifier.Invalid; return ActorIdentifier.Invalid;
var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_dalamud.Objects.GetObjectAddress(gameObjectIdx);
return _actors.AwaitedService.FromObject(ptr, out _, false, true, true); return _actors.AwaitedService.FromObject(ptr, out _, false, true, true);
} }

View file

@ -1,104 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Resolver;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
public class CharacterResolver : IDisposable
{
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly TempCollectionManager _tempCollections;
private readonly ResourceLoader _loader;
private readonly PathResolver _pathResolver;
public unsafe CharacterResolver(Configuration config, ModCollection.Manager collectionManager, TempCollectionManager tempCollections,
ResourceLoader loader, PathResolver pathResolver)
{
_config = config;
_collectionManager = collectionManager;
_tempCollections = tempCollections;
_loader = loader;
_pathResolver = pathResolver;
_loader.ResolvePath = ResolvePath;
_loader.FileLoaded += ImcLoadResource;
}
/// <summary> Obtain a temporary or permanent collection by name. </summary>
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection);
/// <summary> Try to resolve the given game path to the replaced path. </summary>
public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType)
{
// Check if mods are enabled or if we are in a inc-ref at 0 reference count situation.
if (!_config.EnableMods)
return (null, ResolveData.Invalid);
path = path.ToLower();
return category switch
{
// Only Interface collection.
ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path),
_collectionManager.Interface.ToResolveData()),
// Never allow changing scripts.
ResourceCategory.UiScript => (null, ResolveData.Invalid),
ResourceCategory.GameScript => (null, ResolveData.Invalid),
// Use actual resolving.
ResourceCategory.Chara => _pathResolver.CharacterResolver(path, resourceType),
ResourceCategory.Shader => _pathResolver.CharacterResolver(path, resourceType),
ResourceCategory.Vfx => _pathResolver.CharacterResolver(path, resourceType),
ResourceCategory.Sound => _pathResolver.CharacterResolver(path, resourceType),
// None of these files are ever associated with specific characters,
// always use the default resolver for now.
ResourceCategory.Common => DefaultResolver(path),
ResourceCategory.BgCommon => DefaultResolver(path),
ResourceCategory.Bg => DefaultResolver(path),
ResourceCategory.Cut => DefaultResolver(path),
ResourceCategory.Exd => DefaultResolver(path),
ResourceCategory.Music => DefaultResolver(path),
_ => DefaultResolver(path),
};
}
public unsafe void Dispose()
{
_loader.ResetResolvePath();
_loader.FileLoaded -= ImcLoadResource;
}
/// <summary> Use the default method of path replacement. </summary>
private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path)
{
var resolved = _collectionManager.Default.ResolvePath(path);
return (resolved, _collectionManager.Default.ToResolveData());
}
/// <summary> After loading an IMC file, replace its contents with the modded IMC file. </summary>
private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData)
{
if (resource->FileType != ResourceType.Imc)
return;
var lastUnderscore = additionalData.LastIndexOf((byte)'_');
var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString();
if (Utf8GamePath.FromByteString(path, out var gamePath)
&& CollectionByName(name, out var collection)
&& collection.HasCache
&& collection.GetImcFile(gamePath, out var file))
{
file.Replace(resource);
Penumbra.Log.Verbose(
$"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}.");
}
}
}

View file

@ -0,0 +1,308 @@
using System;
using System.Threading;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace Penumbra.Interop.Resolver;
public unsafe class AnimationHookService : IDisposable
{
private readonly PerformanceTracker _performance;
private readonly ObjectTable _objects;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly CollectionResolver _resolver;
private readonly ThreadLocal<ResolveData> _animationLoadData = new(() => ResolveData.Invalid, true);
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
public AnimationHookService(PerformanceTracker performance, ObjectTable objects, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, CollectionResolver resolver)
{
_performance = performance;
_objects = objects;
_collectionResolver = collectionResolver;
_drawObjectState = drawObjectState;
_resolver = resolver;
SignatureHelper.Initialise(this);
_loadCharacterSoundHook.Enable();
_loadTimelineResourcesHook.Enable();
_characterBaseLoadAnimationHook.Enable();
_loadSomePapHook.Enable();
_someActionLoadHook.Enable();
_loadCharacterVfxHook.Enable();
_loadAreaVfxHook.Enable();
_scheduleClipUpdateHook.Enable();
}
public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData)
{
switch (type)
{
case ResourceType.Scd:
if (_characterSoundData.IsValueCreated && _characterSoundData.Value.Valid)
{
resolveData = _characterSoundData.Value;
return true;
}
if (_animationLoadData.IsValueCreated && _animationLoadData.Value.Valid)
{
resolveData = _animationLoadData.Value;
return true;
}
break;
case ResourceType.Tmb:
case ResourceType.Pap:
case ResourceType.Avfx:
case ResourceType.Atex:
if (_animationLoadData.IsValueCreated && _animationLoadData.Value.Valid)
{
resolveData = _animationLoadData.Value;
return true;
}
break;
}
var lastObj = _drawObjectState.LastGameObject;
if (lastObj != nint.Zero)
{
resolveData = _resolver.IdentifyCollection((GameObject*)lastObj, true);
return true;
}
resolveData = ResolveData.Invalid;
return false;
}
public void Dispose()
{
_loadCharacterSoundHook.Dispose();
_loadTimelineResourcesHook.Dispose();
_characterBaseLoadAnimationHook.Dispose();
_loadSomePapHook.Dispose();
_someActionLoadHook.Dispose();
_loadCharacterVfxHook.Dispose();
_loadAreaVfxHook.Dispose();
_scheduleClipUpdateHook.Dispose();
}
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
private delegate IntPtr LoadCharacterSound(IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7);
[Signature(Sigs.LoadCharacterSound, DetourName = nameof(LoadCharacterSoundDetour))]
private readonly Hook<LoadCharacterSound> _loadCharacterSoundHook = null!;
private IntPtr LoadCharacterSoundDetour(IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7)
{
using var performance = _performance.Measure(PerformanceType.LoadSound);
var last = _characterSoundData.Value;
_characterSoundData.Value = _collectionResolver.IdentifyCollection((GameObject*)character, true);
var ret = _loadCharacterSoundHook.Original(character, unk1, unk2, unk3, unk4, unk5, unk6, unk7);
_characterSoundData.Value = last;
return ret;
}
/// <summary>
/// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files.
/// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection.
/// </summary>
private delegate ulong LoadTimelineResourcesDelegate(IntPtr timeline);
[Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))]
private readonly Hook<LoadTimelineResourcesDelegate> _loadTimelineResourcesHook = null!;
private ulong LoadTimelineResourcesDetour(IntPtr timeline)
{
using var performance = _performance.Measure(PerformanceType.TimelineResources);
var last = _animationLoadData.Value;
_animationLoadData.Value = GetDataFromTimeline(timeline);
var ret = _loadTimelineResourcesHook.Original(timeline);
_animationLoadData.Value = last;
return ret;
}
/// <summary>
/// Probably used when the base idle animation gets loaded.
/// Make it aware of the correct collection to load the correct pap files.
/// </summary>
private delegate void CharacterBaseNoArgumentDelegate(IntPtr drawBase);
[Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))]
private readonly Hook<CharacterBaseNoArgumentDelegate> _characterBaseLoadAnimationHook = null!;
private void CharacterBaseLoadAnimationDetour(IntPtr drawObject)
{
using var performance = _performance.Measure(PerformanceType.LoadCharacterBaseAnimation);
var last = _animationLoadData.Value;
var lastObj = _drawObjectState.LastGameObject;
if (lastObj == nint.Zero && _drawObjectState.TryGetValue(drawObject, out var p))
lastObj = p.Item1;
_animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true);
_characterBaseLoadAnimationHook.Original(drawObject);
_animationLoadData.Value = last;
}
/// <summary> Unknown what exactly this is but it seems to load a bunch of paps. </summary>
private delegate void LoadSomePap(IntPtr a1, int a2, IntPtr a3, int a4);
[Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))]
private readonly Hook<LoadSomePap> _loadSomePapHook = null!;
private void LoadSomePapDetour(IntPtr a1, int a2, IntPtr a3, int a4)
{
using var performance = _performance.Measure(PerformanceType.LoadPap);
var timelinePtr = a1 + Offsets.TimeLinePtr;
var last = _animationLoadData.Value;
if (timelinePtr != IntPtr.Zero)
{
var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);
if (actorIdx >= 0 && actorIdx < _objects.Length)
_animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true);
}
_loadSomePapHook.Original(a1, a2, a3, a4);
_animationLoadData.Value = last;
}
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
[Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))]
private readonly Hook<CharacterBaseNoArgumentDelegate> _someActionLoadHook = null!;
private void SomeActionLoadDetour(nint gameObject)
{
using var performance = _performance.Measure(PerformanceType.LoadAction);
var last = _animationLoadData.Value;
_animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)gameObject, true);
_someActionLoadHook.Original(gameObject);
_animationLoadData.Value = last;
}
/// <summary> Load a VFX specifically for a character. </summary>
private delegate IntPtr LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4);
[Signature(Sigs.LoadCharacterVfx, DetourName = nameof(LoadCharacterVfxDetour))]
private readonly Hook<LoadCharacterVfxDelegate> _loadCharacterVfxHook = null!;
private IntPtr LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4)
{
using var performance = _performance.Measure(PerformanceType.LoadCharacterVfx);
var last = _animationLoadData.Value;
if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1))
{
var obj = vfxParams->GameObjectType switch
{
0 => _objects.SearchById(vfxParams->GameObjectId),
2 => _objects[(int)vfxParams->GameObjectId],
4 => GetOwnedObject(vfxParams->GameObjectId),
_ => null,
};
_animationLoadData.Value = obj != null
? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true)
: ResolveData.Invalid;
}
else
{
_animationLoadData.Value = ResolveData.Invalid;
}
var ret = _loadCharacterVfxHook.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4);
#if DEBUG
var path = new ByteString(vfxPath);
Penumbra.Log.Verbose(
$"Load Character VFX: {path} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> "
+ $"0x{ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}");
#endif
_animationLoadData.Value = last;
return ret;
}
/// <summary> Load a ground-based area VFX. </summary>
private delegate nint LoadAreaVfxDelegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3);
[Signature(Sigs.LoadAreaVfx, DetourName = nameof(LoadAreaVfxDetour))]
private readonly Hook<LoadAreaVfxDelegate> _loadAreaVfxHook = null!;
private nint LoadAreaVfxDetour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3)
{
using var performance = _performance.Measure(PerformanceType.LoadAreaVfx);
var last = _animationLoadData.Value;
_animationLoadData.Value = caster != null
? _collectionResolver.IdentifyCollection(caster, true)
: ResolveData.Invalid;
var ret = _loadAreaVfxHook.Original(vfxId, pos, caster, unk1, unk2, unk3);
#if DEBUG
Penumbra.Log.Verbose(
$"Load Area VFX: {vfxId}, {pos[0]} {pos[1]} {pos[2]} {(caster != null ? new ByteString(caster->GetName()).ToString() : "Unknown")} {unk1} {unk2} {unk3}"
+ $" -> {ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}");
#endif
_animationLoadData.Value = last;
return ret;
}
/// <summary> Called when some action timelines update. </summary>
private delegate void ScheduleClipUpdate(ClipScheduler* x);
[Signature(Sigs.ScheduleClipUpdate, DetourName = nameof(ScheduleClipUpdateDetour))]
private readonly Hook<ScheduleClipUpdate> _scheduleClipUpdateHook = null!;
private void ScheduleClipUpdateDetour(ClipScheduler* x)
{
using var performance = _performance.Measure(PerformanceType.ScheduleClipUpdate);
var last = _animationLoadData.Value;
var timeline = x->SchedulerTimeline;
_animationLoadData.Value = GetDataFromTimeline(timeline);
_scheduleClipUpdateHook.Original(x);
_animationLoadData.Value = last;
}
/// <summary> Search an object by its id, then get its minion/mount/ornament. </summary>
private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id)
{
var owner = _objects.SearchById(id);
if (owner == null)
return null;
var idx = ((GameObject*)owner.Address)->ObjectIndex;
return _objects[idx + 1];
}
/// <summary> Use timelines vfuncs to obtain the associated game object. </summary>
private ResolveData GetDataFromTimeline(IntPtr timeline)
{
try
{
if (timeline != IntPtr.Zero)
{
var getGameObjectIdx = ((delegate* unmanaged<IntPtr, int>**)timeline)[0][Offsets.GetGameObjectIdxVfunc];
var idx = getGameObjectIdx(timeline);
if (idx >= 0 && idx < _objects.Length)
{
var obj = (GameObject*)_objects.GetObjectAddress(idx);
return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid;
}
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}");
}
return ResolveData.Invalid;
}
}

View file

@ -0,0 +1,260 @@
using System;
using System.Collections;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.Services;
using Penumbra.Util;
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace Penumbra.Interop.Resolver;
public unsafe class CollectionResolver
{
private readonly PerformanceTracker _performance;
private readonly IdentifiedCollectionCache _cache;
private readonly BitArray _validHumanModels;
private readonly ClientState _clientState;
private readonly GameGui _gameGui;
private readonly ActorService _actors;
private readonly CutsceneService _cutscenes;
private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager;
private readonly TempCollectionManager _tempCollections;
private readonly DrawObjectState _drawObjectState;
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui,
DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager,
TempCollectionManager tempCollections, DrawObjectState drawObjectState)
{
_performance = performance;
_cache = cache;
_clientState = clientState;
_gameGui = gameGui;
_actors = actors;
_cutscenes = cutscenes;
_config = config;
_collectionManager = collectionManager;
_tempCollections = tempCollections;
_drawObjectState = drawObjectState;
_validHumanModels = GetValidHumanModels(gameData);
}
/// <summary>
/// Get the collection applying to the current player character
/// or the Yourself or Default collection if no player exists.
/// </summary>
public ModCollection PlayerCollection()
{
using var performance = _performance.Measure(PerformanceType.IdentifyCollection);
var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero);
if (gameObject == null)
return _collectionManager.ByType(CollectionType.Yourself)
?? _collectionManager.Default;
var player = _actors.AwaitedService.GetCurrentPlayer();
return CollectionByIdentifier(player)
?? CheckYourself(player, gameObject)
?? CollectionByAttributes(gameObject)
?? _collectionManager.Default;
}
/// <summary> Identify the correct collection for a game object. </summary>
public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache)
{
using var t = _performance.Measure(PerformanceType.IdentifyCollection);
if (gameObject == null)
return _collectionManager.Default.ToResolveData();
try
{
if (useCache && _cache.TryGetValue(gameObject, out var data))
return data;
if (LoginScreen(gameObject, out data))
return data;
if (Aesthetician(gameObject, out data))
return data;
return DefaultState(gameObject);
}
catch (Exception ex)
{
Penumbra.Log.Error($"Error identifying collection:\n{ex}");
return _collectionManager.Default.ToResolveData(gameObject);
}
}
/// <summary> Identify the correct collection for the last created game object. </summary>
public ResolveData IdentifyLastGameObjectCollection(bool useCache)
=> IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache);
/// <summary> Identify the correct collection for a draw object. </summary>
public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache)
{
var obj = (GameObject*)(_drawObjectState.TryGetValue((nint)drawObject, out var gameObject)
? gameObject.Item1
: _drawObjectState.LastGameObject);
return IdentifyCollection(obj, useCache);
}
/// <summary> Return whether the given ModelChara id refers to a human-type model. </summary>
public bool IsModelHuman(uint modelCharaId)
=> modelCharaId < _validHumanModels.Length && _validHumanModels[(int)modelCharaId];
/// <summary> Return whether the given character has a human model. </summary>
public bool IsModelHuman(Character* character)
=> character != null && IsModelHuman((uint)character->ModelCharaId);
/// <summary>
/// Used if on the Login screen. Names are populated after actors are drawn,
/// so it is not possible to fetch names from the ui list.
/// Actors are also not named. So use Yourself > Players > Racial > Default.
/// </summary>
private bool LoginScreen(GameObject* gameObject, out ResolveData ret)
{
// Also check for empty names because sometimes named other characters
// might be loaded before being officially logged in.
if (_clientState.IsLoggedIn || gameObject->Name[0] != '\0')
{
ret = ResolveData.Invalid;
return false;
}
var collection2 = _collectionManager.ByType(CollectionType.Yourself)
?? CollectionByAttributes(gameObject)
?? _collectionManager.Default;
ret = _cache.Set(collection2, ActorIdentifier.Invalid, gameObject);
return true;
}
/// <summary> Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. </summary>
private bool Aesthetician(GameObject* gameObject, out ResolveData ret)
{
if (_gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero)
{
ret = ResolveData.Invalid;
return false;
}
var player = _actors.AwaitedService.GetCurrentPlayer();
var collection2 = (player.IsValid ? CollectionByIdentifier(player) : null)
?? _collectionManager.ByType(CollectionType.Yourself)
?? CollectionByAttributes(gameObject)
?? _collectionManager.Default;
ret = _cache.Set(collection2, ActorIdentifier.Invalid, gameObject);
return true;
}
/// <summary>
/// Used when no special state is active.
/// Use individual identifiers first, then Yourself, then group attributes, then ownership settings and last base.
/// </summary>
private ResolveData DefaultState(GameObject* gameObject)
{
var identifier = _actors.AwaitedService.FromObject(gameObject, out var owner, true, false, false);
if (identifier.Type is IdentifierType.Special)
{
(identifier, var type) = _collectionManager.Individuals.ConvertSpecialIdentifier(identifier);
if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect)
return _cache.Set(ModCollection.Empty, identifier, gameObject);
}
var collection = CollectionByIdentifier(identifier)
?? CheckYourself(identifier, gameObject)
?? CollectionByAttributes(gameObject)
?? CheckOwnedCollection(identifier, owner)
?? _collectionManager.Default;
return _cache.Set(collection, identifier, gameObject);
}
/// <summary> Check both temporary and permanent character collections. Temporary first. </summary>
private ModCollection? CollectionByIdentifier(ActorIdentifier identifier)
=> _tempCollections.Collections.TryGetCollection(identifier, out var collection)
|| _collectionManager.Individuals.TryGetCollection(identifier, out collection)
? collection
: null;
/// <summary> Check for the Yourself collection. </summary>
private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor)
{
if (actor->ObjectIndex == 0
|| _cutscenes.GetParentIndex(actor->ObjectIndex) == 0
|| identifier.Equals(_actors.AwaitedService.GetCurrentPlayer()))
return _collectionManager.ByType(CollectionType.Yourself);
return null;
}
/// <summary> Check special collections given the actor. </summary>
private ModCollection? CollectionByAttributes(GameObject* actor)
{
if (!actor->IsCharacter())
return null;
// Only handle human models.
var character = (Character*)actor;
if (!IsModelHuman((uint)character->ModelCharaId))
return null;
var bodyType = character->CustomizeData[2];
var collection = bodyType switch
{
3 => _collectionManager.ByType(CollectionType.NonPlayerElderly),
4 => _collectionManager.ByType(CollectionType.NonPlayerChild),
_ => null,
};
if (collection != null)
return collection;
var race = (SubRace)character->CustomizeData[4];
var gender = (Gender)(character->CustomizeData[1] + 1);
var isNpc = actor->ObjectKind != (byte)ObjectKind.Player;
var type = CollectionTypeExtensions.FromParts(race, gender, isNpc);
collection = _collectionManager.ByType(type);
collection ??= _collectionManager.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
return collection;
}
/// <summary> Get the collection applying to the owner if it is available. </summary>
private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner)
{
if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null)
return null;
var id = _actors.AwaitedService.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld,
ObjectKind.None,
uint.MaxValue);
return CheckYourself(id, owner)
?? CollectionByAttributes(owner);
}
/// <summary>
/// Go through all ModelChara rows and return a bitfield of those that resolve to human models.
/// </summary>
private static BitArray GetValidHumanModels(DataManager gameData)
{
var sheet = gameData.GetExcelSheet<ModelChara>()!;
var ret = new BitArray((int)sheet.RowCount, false);
foreach (var (_, idx) in sheet.WithIndex().Where(p => p.Value.Type == (byte)CharacterBase.ModelType.Human))
ret[idx] = true;
return ret;
}
}

View file

@ -4,15 +4,16 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.Interop.Services; using Penumbra.GameData.Actors;
using Penumbra.Interop.Services;
namespace Penumbra.Interop.Resolver; namespace Penumbra.Interop.Resolver;
public class CutsceneService : IDisposable public class CutsceneService : IDisposable
{ {
public const int CutsceneStartIdx = 200; public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart;
public const int CutsceneSlots = 40; public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
private readonly GameEventManager _events; private readonly GameEventManager _events;
private readonly ObjectTable _objects; private readonly ObjectTable _objects;
@ -23,16 +24,19 @@ public class CutsceneService : IDisposable
.Where(i => _objects[i] != null) .Where(i => _objects[i] != null)
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!)); .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
public CutsceneService(ObjectTable objects, GameEventManager events) public unsafe CutsceneService(ObjectTable objects, GameEventManager events)
{ {
_objects = objects; _objects = objects;
_events = events; _events = events;
Enable(); _events.CopyCharacter += OnCharacterCopy;
_events.CharacterDestructor += OnCharacterDestructor;
} }
// Get the related actor to a cutscene actor. /// <summary>
// Does not check for valid input index. /// Get the related actor to a cutscene actor.
// Returns null if no connected actor is set or the actor does not exist anymore. /// Does not check for valid input index.
/// Returns null if no connected actor is set or the actor does not exist anymore.
/// </summary>
public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx] public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx]
{ {
get get
@ -43,7 +47,7 @@ public class CutsceneService : IDisposable
} }
} }
// Return the currently set index of a parent or -1 if none is set or the index is invalid. /// <summary> Return the currently set index of a parent or -1 if none is set or the index is invalid. </summary>
public int GetParentIndex(int idx) public int GetParentIndex(int idx)
{ {
if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) if (idx is >= CutsceneStartIdx and < CutsceneEndIdx)
@ -52,21 +56,12 @@ public class CutsceneService : IDisposable
return -1; return -1;
} }
public unsafe void Enable() public unsafe void Dispose()
{
_events.CopyCharacter += OnCharacterCopy;
_events.CharacterDestructor += OnCharacterDestructor;
}
public unsafe void Disable()
{ {
_events.CopyCharacter -= OnCharacterCopy; _events.CopyCharacter -= OnCharacterCopy;
_events.CharacterDestructor -= OnCharacterDestructor; _events.CharacterDestructor -= OnCharacterDestructor;
} }
public void Dispose()
=> Disable();
private unsafe void OnCharacterDestructor(Character* character) private unsafe void OnCharacterDestructor(Character* character)
{ {
if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx) if (character->GameObject.ObjectIndex is < CutsceneStartIdx or >= CutsceneEndIdx)

View file

@ -0,0 +1,150 @@
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Services;
using Penumbra.String.Classes;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
namespace Penumbra.Interop.Resolver;
public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, bool)>
{
private readonly ObjectTable _objects;
private readonly GameEventManager _gameEvents;
private readonly Dictionary<nint, (nint GameObject, bool IsChild)> _drawObjectToGameObject = new();
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
public nint LastGameObject
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;
public DrawObjectState(ObjectTable objects, GameEventManager gameEvents)
{
SignatureHelper.Initialise(this);
_enableDrawHook.Enable();
_objects = objects;
_gameEvents = gameEvents;
_gameEvents.WeaponReloading += OnWeaponReloading;
_gameEvents.WeaponReloaded += OnWeaponReloaded;
_gameEvents.CharacterBaseCreated += OnCharacterBaseCreated;
_gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor;
InitializeDrawObjects();
}
public bool ContainsKey(nint key)
=> _drawObjectToGameObject.ContainsKey(key);
public IEnumerator<KeyValuePair<nint, (nint, bool)>> GetEnumerator()
=> _drawObjectToGameObject.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _drawObjectToGameObject.Count;
public bool TryGetValue(nint drawObject, out (nint, bool) gameObject)
=> _drawObjectToGameObject.TryGetValue(drawObject, out gameObject);
public (nint, bool) this[nint key]
=> _drawObjectToGameObject[key];
public IEnumerable<nint> Keys
=> _drawObjectToGameObject.Keys;
public IEnumerable<(nint, bool)> Values
=> _drawObjectToGameObject.Values;
public void Dispose()
{
_gameEvents.WeaponReloading -= OnWeaponReloading;
_gameEvents.WeaponReloaded -= OnWeaponReloaded;
_gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated;
_gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor;
_enableDrawHook.Dispose();
}
private void OnWeaponReloading(nint _, nint gameObject)
=> _lastGameObject.Value!.Enqueue(gameObject);
private unsafe void OnWeaponReloaded(nint _, nint gameObject)
{
_lastGameObject.Value!.Dequeue();
IterateDrawObjectTree((Object*) ((GameObject*) gameObject)->DrawObject, gameObject, false, false);
}
private void OnCharacterBaseDestructor(nint characterBase)
=> _drawObjectToGameObject.Remove(characterBase);
private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject)
{
var gameObject = LastGameObject;
if (gameObject != nint.Zero)
_drawObjectToGameObject[drawObject] = (gameObject, false);
}
/// <summary>
/// Find all current DrawObjects used in the GameObject table.
/// We do not iterate the Dalamud table because it does not work when not logged in.
/// </summary>
private unsafe void InitializeDrawObjects()
{
for (var i = 0; i < _objects.Length; ++i)
{
var ptr = (GameObject*)_objects.GetObjectAddress(i);
if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null)
IterateDrawObjectTree(&ptr->DrawObject->Object, (nint)ptr, false, false);
}
}
private unsafe void IterateDrawObjectTree(Object* drawObject, nint gameObject, bool isChild, bool iterate)
{
if (drawObject == null)
return;
_drawObjectToGameObject[(nint)drawObject] = (gameObject, isChild);
IterateDrawObjectTree(drawObject->ChildObject, gameObject, true, true);
if (!iterate)
return;
var nextSibling = drawObject->NextSiblingObject;
while (nextSibling != null && nextSibling != drawObject)
{
IterateDrawObjectTree(nextSibling, gameObject, true, false);
nextSibling = nextSibling->NextSiblingObject;
}
var prevSibling = drawObject->PreviousSiblingObject;
while (prevSibling != null && prevSibling != drawObject)
{
IterateDrawObjectTree(prevSibling, gameObject, true, false);
prevSibling = prevSibling->PreviousSiblingObject;
}
}
/// <summary>
/// EnableDraw is what creates DrawObjects for gameObjects,
/// so we always keep track of the current GameObject to be able to link it to the DrawObject.
/// </summary>
private delegate void EnableDrawDelegate(nint gameObject, nint b, nint c, nint d);
[Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))]
private readonly Hook<EnableDrawDelegate> _enableDrawHook = null!;
private void EnableDrawDetour(nint gameObject, nint b, nint c, nint d)
{
_lastGameObject.Value!.Enqueue(gameObject);
_enableDrawHook.Original.Invoke(gameObject, b, c, d);
_lastGameObject.Value!.TryDequeue(out _);
}
}

View file

@ -13,41 +13,21 @@ namespace Penumbra.Interop.Resolver;
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly GameEventManager _events; private readonly GameEventManager _events;
private readonly ClientState _clientState; private readonly ClientState _clientState;
private readonly Dictionary<IntPtr, (ActorIdentifier, ModCollection)> _cache = new(317); private readonly Dictionary<nint, (ActorIdentifier, ModCollection)> _cache = new(317);
private bool _dirty; private bool _dirty;
private bool _enabled;
public IdentifiedCollectionCache(ClientState clientState, CommunicatorService communicator, GameEventManager events) public IdentifiedCollectionCache(ClientState clientState, CommunicatorService communicator, GameEventManager events)
{ {
_clientState = clientState; _clientState = clientState;
_communicator = communicator; _communicator = communicator;
_events = events; _events = events;
Enable();
}
public void Enable()
{
if (_enabled)
return;
_communicator.CollectionChange.Event += CollectionChangeClear; _communicator.CollectionChange.Event += CollectionChangeClear;
_clientState.TerritoryChanged += TerritoryClear; _clientState.TerritoryChanged += TerritoryClear;
_events.CharacterDestructor += OnCharacterDestruct; _events.CharacterDestructor += OnCharacterDestruct;
_enabled = true;
}
public void Disable()
{
if (!_enabled)
return;
_communicator.CollectionChange.Event -= CollectionChangeClear;
_clientState.TerritoryChanged -= TerritoryClear;
_events.CharacterDestructor -= OnCharacterDestruct;
_enabled = false;
} }
public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data) public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data)
@ -81,8 +61,9 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A
public void Dispose() public void Dispose()
{ {
Disable(); _communicator.CollectionChange.Event -= CollectionChangeClear;
GC.SuppressFinalize(this); _clientState.TerritoryChanged -= TerritoryClear;
_events.CharacterDestructor -= OnCharacterDestruct;
} }
public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator() public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator()
@ -96,9 +77,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A
} }
} }
~IdentifiedCollectionCache()
=> Dispose();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();

View file

@ -0,0 +1,267 @@
using System;
using System.Linq;
using System.Threading;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader;
using Penumbra.Interop.Services;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Util;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
using static Penumbra.GameData.Enums.GenderRace;
namespace Penumbra.Interop.Resolver;
// State: 6.35
// GetSlotEqpData seems to be the only function using the EQP table.
// It is only called by CheckSlotsForUnload (called by UpdateModels),
// SetupModelAttributes (called by UpdateModels and OnModelLoadComplete)
// and a unnamed function called by UpdateRender.
// It seems to be enough to change the EQP entries for UpdateModels.
// GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables.
// They are called by ResolveMdlPath, UpdateModels and SetupConnectorModelAttributes,
// which is called by SetupModelAttributes, which is called by OnModelLoadComplete and UpdateModels.
// It seems to be enough to change EQDP on UpdateModels and ResolveMDLPath.
// EST entries seem to be obtained by "44 8B C9 83 EA ?? 74", which is only called by
// ResolveSKLBPath, ResolveSKPPath, ResolvePHYBPath and indirectly by ResolvePAPPath.
// RSP height entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF"
// RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05"
// RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF"
// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create,
// ChangeCustomize and RspSetupCharacter, which is hooked here.
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter.
public unsafe class MetaState : IDisposable
{
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _humanVTable = null!;
private readonly CommunicatorService _communicator;
private readonly PerformanceTracker _performance;
private readonly CollectionResolver _collectionResolver;
private readonly ResourceService _resources;
private readonly GameEventManager _gameEventManager;
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty;
public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver,
ResourceService resources, GameEventManager gameEventManager)
{
_performance = performance;
_communicator = communicator;
_collectionResolver = collectionResolver;
_resources = resources;
_gameEventManager = gameEventManager;
SignatureHelper.Initialise(this);
_onModelLoadCompleteHook = Hook<OnModelLoadCompleteDelegate>.FromAddress(_humanVTable[58], OnModelLoadCompleteDetour);
_getEqpIndirectHook.Enable();
_updateModelsHook.Enable();
_onModelLoadCompleteHook.Enable();
_setupVisorHook.Enable();
_rspSetupCharacterHook.Enable();
_changeCustomize.Enable();
_gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase;
_gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated;
}
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
{
if (type == ResourceType.Tex
&& _lastCreatedCollection.Valid
&& gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
{
resolveData = _lastCreatedCollection;
return true;
}
resolveData = ResolveData.Invalid;
return false;
}
public static DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
{
var races = race.Dependencies();
if (races.Length == 0)
return DisposableContainer.Empty;
var equipmentEnumerable = equipment
? races.Select(r => collection.TemporarilySetEqdpFile(r, false))
: Array.Empty<IDisposable?>().AsEnumerable();
var accessoryEnumerable = accessory
? races.Select(r => collection.TemporarilySetEqdpFile(r, true))
: Array.Empty<IDisposable?>().AsEnumerable();
return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable));
}
public static GenderRace GetHumanGenderRace(nint human)
=> (GenderRace)((Human*)human)->RaceSexId;
public void Dispose()
{
_getEqpIndirectHook.Dispose();
_updateModelsHook.Dispose();
_onModelLoadCompleteHook.Dispose();
_setupVisorHook.Dispose();
_rspSetupCharacterHook.Dispose();
_changeCustomize.Dispose();
_gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase;
_gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated;
}
private void OnCreatingCharacterBase(uint modelCharaId, nint customize, nint equipData)
{
_lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true);
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
_communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection.Name, (nint)(&modelCharaId), customize, equipData);
var decal = new CharacterUtility.DecalReverter(_resources, _lastCreatedCollection.ModCollection, UsesDecal(modelCharaId, equipData));
var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile();
_characterBaseCreateMetaChanges.Dispose(); // Should always be empty.
_characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp);
}
private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject)
{
_characterBaseCreateMetaChanges.Dispose();
_characterBaseCreateMetaChanges = DisposableContainer.Empty;
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
_lastCreatedCollection.ModCollection.Name, drawObject);
_lastCreatedCollection = ResolveData.Invalid;
}
private delegate void OnModelLoadCompleteDelegate(nint drawObject);
private readonly Hook<OnModelLoadCompleteDelegate> _onModelLoadCompleteHook;
private void OnModelLoadCompleteDetour(nint drawObject)
{
var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var eqp = collection.ModCollection.TemporarilySetEqpFile();
using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true);
_onModelLoadCompleteHook.Original.Invoke(drawObject);
}
private delegate void UpdateModelDelegate(nint drawObject);
[Signature(Sigs.UpdateModel, DetourName = nameof(UpdateModelsDetour))]
private readonly Hook<UpdateModelDelegate> _updateModelsHook = null!;
private void UpdateModelsDetour(nint drawObject)
{
// Shortcut because this is called all the time.
// Same thing is checked at the beginning of the original function.
if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0)
return;
using var performance = _performance.Measure(PerformanceType.UpdateModels);
var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var eqp = collection.ModCollection.TemporarilySetEqpFile();
using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true);
_updateModelsHook.Original.Invoke(drawObject);
}
private static GenderRace GetDrawObjectGenderRace(nint drawObject)
{
var draw = (DrawObject*)drawObject;
if (draw->Object.GetObjectType() != ObjectType.CharacterBase)
return Unknown;
var c = (CharacterBase*)drawObject;
return c->GetModelType() == CharacterBase.ModelType.Human
? GetHumanGenderRace(drawObject)
: Unknown;
}
[Signature(Sigs.GetEqpIndirect, DetourName = nameof(GetEqpIndirectDetour))]
private readonly Hook<OnModelLoadCompleteDelegate> _getEqpIndirectHook = null!;
private void GetEqpIndirectDetour(nint drawObject)
{
// Shortcut because this is also called all the time.
// Same thing is checked at the beginning of the original function.
if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0)
return;
using var performance = _performance.Measure(PerformanceType.GetEqp);
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var eqp = resolveData.ModCollection.TemporarilySetEqpFile();
_getEqpIndirectHook.Original(drawObject);
}
// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
// but it only applies a changed gmp file after a redraw for some reason.
private delegate byte SetupVisorDelegate(nint drawObject, ushort modelId, byte visorState);
[Signature(Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
private readonly Hook<SetupVisorDelegate> _setupVisorHook = null!;
private byte SetupVisorDetour(nint drawObject, ushort modelId, byte visorState)
{
using var performance = _performance.Measure(PerformanceType.SetupVisor);
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var gmp = resolveData.ModCollection.TemporarilySetGmpFile();
return _setupVisorHook.Original(drawObject, modelId, visorState);
}
// RSP
private delegate void RspSetupCharacterDelegate(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5);
[Signature(Sigs.RspSetupCharacter, DetourName = nameof(RspSetupCharacterDetour))]
private readonly Hook<RspSetupCharacterDelegate> _rspSetupCharacterHook = null!;
private void RspSetupCharacterDetour(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5)
{
if (_inChangeCustomize)
{
_rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5);
}
else
{
using var performance = _performance.Measure(PerformanceType.SetupCharacter);
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile();
_rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5);
}
}
/// <summary> ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change. </summary>
private bool _inChangeCustomize;
private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment);
[Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))]
private readonly Hook<ChangeCustomizeDelegate> _changeCustomize = null!;
private bool ChangeCustomizeDetour(nint human, nint data, byte skipEquipment)
{
using var performance = _performance.Measure(PerformanceType.ChangeCustomize);
_inChangeCustomize = true;
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)human, true);
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile();
using var decals =
new CharacterUtility.DecalReverter(_resources, resolveData.ModCollection, UsesDecal(0, data));
var ret = _changeCustomize.Original(human, data, skipEquipment);
_inChangeCustomize = false;
return ret;
}
/// <summary>
/// Check the customize array for the FaceCustomization byte and the last bit of that.
/// Also check for humans.
/// </summary>
public static bool UsesDecal(uint modelId, nint customizeData)
=> modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
}

View file

@ -1,350 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class AnimationState
{
private readonly DrawObjectState _drawObjectState;
private ResolveData _animationLoadData = ResolveData.Invalid;
private ResolveData _characterSoundData = ResolveData.Invalid;
public AnimationState( DrawObjectState drawObjectState )
{
_drawObjectState = drawObjectState;
SignatureHelper.Initialise( this );
}
public bool HandleFiles( ResourceType type, Utf8GamePath _, out ResolveData resolveData )
{
switch( type )
{
case ResourceType.Scd:
if( _characterSoundData.Valid )
{
resolveData = _characterSoundData;
return true;
}
if( _animationLoadData.Valid )
{
resolveData = _animationLoadData;
return true;
}
break;
case ResourceType.Tmb:
case ResourceType.Pap:
case ResourceType.Avfx:
case ResourceType.Atex:
if( _animationLoadData.Valid )
{
resolveData = _animationLoadData;
return true;
}
break;
}
if( _drawObjectState.LastGameObject != null )
{
resolveData = _drawObjectState.LastCreatedCollection;
return true;
}
resolveData = ResolveData.Invalid;
return false;
}
public void Enable()
{
_loadTimelineResourcesHook.Enable();
_characterBaseLoadAnimationHook.Enable();
_loadSomePapHook.Enable();
_someActionLoadHook.Enable();
_loadCharacterSoundHook.Enable();
_loadCharacterVfxHook.Enable();
_loadAreaVfxHook.Enable();
_scheduleClipUpdateHook.Enable();
//_loadSomeAvfxHook.Enable();
//_someOtherAvfxHook.Enable();
}
public void Disable()
{
_loadTimelineResourcesHook.Disable();
_characterBaseLoadAnimationHook.Disable();
_loadSomePapHook.Disable();
_someActionLoadHook.Disable();
_loadCharacterSoundHook.Disable();
_loadCharacterVfxHook.Disable();
_loadAreaVfxHook.Disable();
_scheduleClipUpdateHook.Disable();
//_loadSomeAvfxHook.Disable();
//_someOtherAvfxHook.Disable();
}
public void Dispose()
{
_loadTimelineResourcesHook.Dispose();
_characterBaseLoadAnimationHook.Dispose();
_loadSomePapHook.Dispose();
_someActionLoadHook.Dispose();
_loadCharacterSoundHook.Dispose();
_loadCharacterVfxHook.Dispose();
_loadAreaVfxHook.Dispose();
_scheduleClipUpdateHook.Dispose();
//_loadSomeAvfxHook.Dispose();
//_someOtherAvfxHook.Dispose();
}
// Characters load some of their voice lines or whatever with this function.
private delegate IntPtr LoadCharacterSound( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 );
[Signature( Sigs.LoadCharacterSound, DetourName = nameof( LoadCharacterSoundDetour ) )]
private readonly Hook< LoadCharacterSound > _loadCharacterSoundHook = null!;
private IntPtr LoadCharacterSoundDetour( IntPtr character, int unk1, int unk2, IntPtr unk3, ulong unk4, int unk5, int unk6, ulong unk7 )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadSound );
var last = _characterSoundData;
_characterSoundData = IdentifyCollection( ( GameObject* )character, true );
var ret = _loadCharacterSoundHook.Original( character, unk1, unk2, unk3, unk4, unk5, unk6, unk7 );
_characterSoundData = last;
return ret;
}
private static ResolveData GetDataFromTimeline( IntPtr timeline )
{
try
{
if( timeline != IntPtr.Zero )
{
var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ Offsets.GetGameObjectIdxVfunc ];
var idx = getGameObjectIdx( timeline );
if( idx >= 0 && idx < DalamudServices.SObjects.Length )
{
var obj = DalamudServices.SObjects[ idx ];
return obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid;
}
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Error getting timeline data for 0x{timeline:X}:\n{e}" );
}
return ResolveData.Invalid;
}
// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files.
// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection.
private delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline );
[Signature( Sigs.LoadTimelineResources, DetourName = nameof( LoadTimelineResourcesDetour ) )]
private readonly Hook< LoadTimelineResourcesDelegate > _loadTimelineResourcesHook = null!;
private ulong LoadTimelineResourcesDetour( IntPtr timeline )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.TimelineResources );
var old = _animationLoadData;
_animationLoadData = GetDataFromTimeline( timeline );
var ret = _loadTimelineResourcesHook.Original( timeline );
_animationLoadData = old;
return ret;
}
// Probably used when the base idle animation gets loaded.
// Make it aware of the correct collection to load the correct pap files.
private delegate void CharacterBaseNoArgumentDelegate( IntPtr drawBase );
[Signature( Sigs.CharacterBaseLoadAnimation, DetourName = nameof( CharacterBaseLoadAnimationDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _characterBaseLoadAnimationHook = null!;
private void CharacterBaseLoadAnimationDetour( IntPtr drawObject )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadCharacterBaseAnimation );
var last = _animationLoadData;
_animationLoadData = _drawObjectState.LastCreatedCollection.Valid
? _drawObjectState.LastCreatedCollection
: FindParent( drawObject, out var collection ) != null
? collection
: Penumbra.CollectionManager.Default.ToResolveData();
_characterBaseLoadAnimationHook.Original( drawObject );
_animationLoadData = last;
}
// Unknown what exactly this is but it seems to load a bunch of paps.
private delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 );
[Signature( Sigs.LoadSomePap, DetourName = nameof( LoadSomePapDetour ) )]
private readonly Hook< LoadSomePap > _loadSomePapHook = null!;
private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadPap );
var timelinePtr = a1 + Offsets.TimeLinePtr;
var last = _animationLoadData;
if( timelinePtr != IntPtr.Zero )
{
var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 );
if( actorIdx >= 0 && actorIdx < DalamudServices.SObjects.Length )
{
_animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.SObjects[ actorIdx ]?.Address ?? IntPtr.Zero ), true );
}
}
_loadSomePapHook.Original( a1, a2, a3, a4 );
_animationLoadData = last;
}
// Seems to load character actions when zoning or changing class, maybe.
[Signature( Sigs.LoadSomeAction, DetourName = nameof( SomeActionLoadDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _someActionLoadHook = null!;
private void SomeActionLoadDetour( IntPtr gameObject )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadAction );
var last = _animationLoadData;
_animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true );
_someActionLoadHook.Original( gameObject );
_animationLoadData = last;
}
private delegate IntPtr LoadCharacterVfxDelegate( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 );
[Signature( Sigs.LoadCharacterVfx, DetourName = nameof( LoadCharacterVfxDetour ) )]
private readonly Hook< LoadCharacterVfxDelegate > _loadCharacterVfxHook = null!;
private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id )
{
var owner = DalamudServices.SObjects.SearchById( id );
if( owner == null )
{
return null;
}
var idx = ( ( GameObject* )owner.Address )->ObjectIndex;
return DalamudServices.SObjects[ idx + 1 ];
}
private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadCharacterVfx );
var last = _animationLoadData;
if( vfxParams != null && vfxParams->GameObjectId != unchecked( ( uint )-1 ) )
{
var obj = vfxParams->GameObjectType switch
{
0 => DalamudServices.SObjects.SearchById( vfxParams->GameObjectId ),
2 => DalamudServices.SObjects[ ( int )vfxParams->GameObjectId ],
4 => GetOwnedObject( vfxParams->GameObjectId ),
_ => null,
};
_animationLoadData = obj != null
? IdentifyCollection( ( GameObject* )obj.Address, true )
: ResolveData.Invalid;
}
else
{
_animationLoadData = ResolveData.Invalid;
}
var ret = _loadCharacterVfxHook.Original( vfxPath, vfxParams, unk1, unk2, unk3, unk4 );
#if DEBUG
var path = new ByteString( vfxPath );
Penumbra.Log.Verbose(
$"Load Character VFX: {path} {vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" );
#endif
_animationLoadData = last;
return ret;
}
private delegate IntPtr LoadAreaVfxDelegate( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 );
[Signature( Sigs.LoadAreaVfx, DetourName = nameof( LoadAreaVfxDetour ) )]
private readonly Hook< LoadAreaVfxDelegate > _loadAreaVfxHook = null!;
private IntPtr LoadAreaVfxDetour( uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3 )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.LoadAreaVfx );
var last = _animationLoadData;
if( caster != null )
{
_animationLoadData = IdentifyCollection( caster, true );
}
else
{
_animationLoadData = ResolveData.Invalid;
}
var ret = _loadAreaVfxHook.Original( vfxId, pos, caster, unk1, unk2, unk3 );
#if DEBUG
Penumbra.Log.Verbose(
$"Load Area VFX: {vfxId}, {pos[ 0 ]} {pos[ 1 ]} {pos[ 2 ]} {( caster != null ? new ByteString( caster->GetName() ).ToString() : "Unknown" )} {unk1} {unk2} {unk3} -> {ret:X} {_animationLoadData.ModCollection.Name} {_animationLoadData.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}" );
#endif
_animationLoadData = last;
return ret;
}
private delegate void ScheduleClipUpdate( ClipScheduler* x );
[Signature( Sigs.ScheduleClipUpdate, DetourName = nameof( ScheduleClipUpdateDetour ) )]
private readonly Hook< ScheduleClipUpdate > _scheduleClipUpdateHook = null!;
private void ScheduleClipUpdateDetour( ClipScheduler* x )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.ScheduleClipUpdate );
var old = _animationLoadData;
var timeline = x->SchedulerTimeline;
_animationLoadData = GetDataFromTimeline( timeline );
_scheduleClipUpdateHook.Original( x );
_animationLoadData = old;
}
// ========== Those hooks seem to be superseded by LoadCharacterVfx =========
// public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 );
//
// [Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )]
// private readonly Hook<LoadSomeAvfx> _loadSomeAvfxHook = null!;
//
// private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 )
// {
// var last = _animationLoadData;
// _animationLoadData = IdentifyCollection( ( GameObject* )gameObject, true );
// var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 );
// _animationLoadData = last;
// return ret;
// }
//
// [Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )]
// private readonly Hook<CharacterBaseNoArgumentDelegate> _someOtherAvfxHook = null!;
//
// private void SomeOtherAvfxDetour( IntPtr unk )
// {
// var last = _animationLoadData;
// var gameObject = ( GameObject* )( unk - 0x8D0 );
// _animationLoadData = IdentifyCollection( gameObject, true );
// _someOtherAvfxHook.Original( unk );
// _animationLoadData = last;
// }
}
}

View file

@ -1,255 +0,0 @@
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.String.Classes;
using Penumbra.Util;
using Penumbra.Services;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class DrawObjectState
{
private readonly CommunicatorService _communicator;
public static event CreatingCharacterBaseDelegate? CreatingCharacterBase;
public static event CreatedCharacterBaseDelegate? CreatedCharacterBase;
public IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjects
=> _drawObjectToObject;
public int Count
=> _drawObjectToObject.Count;
public bool TryGetValue(IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject)
{
gameObject = null;
if (!_drawObjectToObject.TryGetValue(drawObject, out value))
return false;
var gameObjectIdx = value.Item2;
return VerifyEntry(drawObject, gameObjectIdx, out gameObject);
}
// Set and update a parent object if it exists and a last game object is set.
public ResolveData CheckParentDrawObject(IntPtr drawObject, IntPtr parentObject)
{
if (parentObject == IntPtr.Zero && LastGameObject != null)
{
var collection = IdentifyCollection(LastGameObject, true);
_drawObjectToObject[drawObject] = (collection, LastGameObject->ObjectIndex);
return collection;
}
return ResolveData.Invalid;
}
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
{
if (type == ResourceType.Tex
&& LastCreatedCollection.Valid
&& gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
{
resolveData = LastCreatedCollection;
return true;
}
resolveData = ResolveData.Invalid;
return false;
}
public ResolveData LastCreatedCollection
=> _lastCreatedCollection;
public GameObject* LastGameObject { get; private set; }
public DrawObjectState(CommunicatorService communicator)
{
SignatureHelper.Initialise(this);
_communicator = communicator;
}
public void Enable()
{
_characterBaseCreateHook.Enable();
_characterBaseDestructorHook.Enable();
_enableDrawHook.Enable();
_weaponReloadHook.Enable();
InitializeDrawObjects();
_communicator.CollectionChange.Event += CheckCollections;
}
public void Disable()
{
_characterBaseCreateHook.Disable();
_characterBaseDestructorHook.Disable();
_enableDrawHook.Disable();
_weaponReloadHook.Disable();
_communicator.CollectionChange.Event -= CheckCollections;
}
public void Dispose()
{
Disable();
_characterBaseCreateHook.Dispose();
_characterBaseDestructorHook.Dispose();
_enableDrawHook.Dispose();
_weaponReloadHook.Dispose();
}
// Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it.
private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject)
{
gameObject = (GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx);
var draw = (DrawObject*)drawObject;
if (gameObject != null
&& (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject))
return true;
gameObject = null;
_drawObjectToObject.Remove(drawObject);
return false;
}
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
// It contains any DrawObjects that correspond to a human actor, even those without specific collections.
private readonly Dictionary<IntPtr, (ResolveData, int)> _drawObjectToObject = new();
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
// Keep track of created DrawObjects that are CharacterBase,
// and use the last game object that called EnableDraw to link them.
private delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d);
[Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))]
private readonly Hook<CharacterBaseCreateDelegate> _characterBaseCreateHook = null!;
private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d)
{
using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterBaseCreate);
var meta = DisposableContainer.Empty;
if (LastGameObject != null)
{
_lastCreatedCollection = IdentifyCollection(LastGameObject, false);
// Change the transparent or 1.0 Decal if necessary.
var decal = new CharacterUtility.DecalReverter(Penumbra.ResourceService, _lastCreatedCollection.ModCollection, UsesDecal(a, c));
// Change the rsp parameters.
meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal);
try
{
var modelPtr = &a;
CreatingCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, (IntPtr)modelPtr, b, c);
}
catch (Exception e)
{
Penumbra.Log.Error($"Unknown Error during CreatingCharacterBase:\n{e}");
}
}
var ret = _characterBaseCreateHook.Original(a, b, c, d);
try
{
if (LastGameObject != null && ret != IntPtr.Zero)
{
_drawObjectToObject[ret] = (_lastCreatedCollection!, LastGameObject->ObjectIndex);
CreatedCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret);
}
}
finally
{
meta.Dispose();
}
return ret;
}
// Check the customize array for the FaceCustomization byte and the last bit of that.
// Also check for humans.
public static bool UsesDecal(uint modelId, IntPtr customizeData)
=> modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
// Remove DrawObjects from the list when they are destroyed.
private delegate void CharacterBaseDestructorDelegate(IntPtr drawBase);
[Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))]
private readonly Hook<CharacterBaseDestructorDelegate> _characterBaseDestructorHook = null!;
private void CharacterBaseDestructorDetour(IntPtr drawBase)
{
_drawObjectToObject.Remove(drawBase);
_characterBaseDestructorHook!.Original.Invoke(drawBase);
}
// EnableDraw is what creates DrawObjects for gameObjects,
// so we always keep track of the current GameObject to be able to link it to the DrawObject.
private delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d);
[Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))]
private readonly Hook<EnableDrawDelegate> _enableDrawHook = null!;
private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d)
{
var oldObject = LastGameObject;
LastGameObject = (GameObject*)gameObject;
_enableDrawHook!.Original.Invoke(gameObject, b, c, d);
LastGameObject = oldObject;
}
// Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8,
// so we use that.
private delegate void WeaponReloadFunc(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7);
[Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))]
private readonly Hook<WeaponReloadFunc> _weaponReloadHook = null!;
public void WeaponReloadDetour(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7)
{
var oldGame = LastGameObject;
LastGameObject = *(GameObject**)(a1 + 8);
_weaponReloadHook!.Original(a1, a2, a3, a4, a5, a6, a7);
LastGameObject = oldGame;
}
// Update collections linked to Game/DrawObjects due to a change in collection configuration.
private void CheckCollections(CollectionType type, ModCollection? _1, ModCollection? _2, string _3)
{
if (type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface)
return;
foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray())
{
if (!VerifyEntry(key, idx, out var obj))
_drawObjectToObject.Remove(key);
var newCollection = IdentifyCollection(obj, false);
_drawObjectToObject[key] = (newCollection, idx);
}
}
// Find all current DrawObjects used in the GameObject table.
// We do not iterate the Dalamud table because it does not work when not logged in.
private void InitializeDrawObjects()
{
for (var i = 0; i < DalamudServices.SObjects.Length; ++i)
{
var ptr = (GameObject*)DalamudServices.SObjects.GetObjectAddress(i);
if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null)
_drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex);
}
}
}
}

View file

@ -1,187 +0,0 @@
using System;
using System.Collections;
using System.Linq;
using Dalamud.Data;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using Penumbra.Collections;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.Services;
using Penumbra.Util;
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
// Identify the correct collection for a GameObject by index and name.
public static ResolveData IdentifyCollection( GameObject* gameObject, bool useCache )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection );
if( gameObject == null )
{
return new ResolveData( Penumbra.CollectionManager.Default );
}
try
{
if( useCache && IdentifiedCache.TryGetValue( gameObject, out var data ) )
{
return data;
}
// Login screen. Names are populated after actors are drawn,
// so it is not possible to fetch names from the ui list.
// Actors are also not named. So use Yourself > Players > Racial > Default.
if( !DalamudServices.SClientState.IsLoggedIn )
{
var collection2 = Penumbra.CollectionManager.ByType( CollectionType.Yourself )
?? CollectionByAttributes( gameObject )
?? Penumbra.CollectionManager.Default;
return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject );
}
// Aesthetician. The relevant actor is yourself, so use player collection when possible.
if( DalamudServices.GameGui.GetAddonByName( "ScreenLog" ) == IntPtr.Zero )
{
var player = Penumbra.Actors.GetCurrentPlayer();
var collection2 = ( player.IsValid ? CollectionByIdentifier( player ) : null )
?? Penumbra.CollectionManager.ByType( CollectionType.Yourself )
?? CollectionByAttributes( gameObject )
?? Penumbra.CollectionManager.Default;
return IdentifiedCache.Set( collection2, ActorIdentifier.Invalid, gameObject );
}
var identifier = Penumbra.Actors.FromObject( gameObject, out var owner, true, false, false );
if( identifier.Type is IdentifierType.Special )
{
( identifier, var type ) = Penumbra.CollectionManager.Individuals.ConvertSpecialIdentifier( identifier );
if( Penumbra.Config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect )
{
return IdentifiedCache.Set( ModCollection.Empty, identifier, gameObject );
}
}
var collection = CollectionByIdentifier( identifier )
?? CheckYourself( identifier, gameObject )
?? CollectionByAttributes( gameObject )
?? CheckOwnedCollection( identifier, owner )
?? Penumbra.CollectionManager.Default;
return IdentifiedCache.Set( collection, identifier, gameObject );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Error identifying collection:\n{e}" );
return Penumbra.CollectionManager.Default.ToResolveData( gameObject );
}
}
// Get the collection applying to the current player character
// or the default collection if no player exists.
public static ModCollection PlayerCollection()
{
using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection );
var gameObject = ( GameObject* )DalamudServices.SObjects.GetObjectAddress( 0 );
if( gameObject == null )
{
return Penumbra.CollectionManager.ByType( CollectionType.Yourself )
?? Penumbra.CollectionManager.Default;
}
var player = Penumbra.Actors.GetCurrentPlayer();
return CollectionByIdentifier( player )
?? CheckYourself( player, gameObject )
?? CollectionByAttributes( gameObject )
?? Penumbra.CollectionManager.Default;
}
// Check both temporary and permanent character collections. Temporary first.
private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier )
=> Penumbra.TempCollections.Collections.TryGetCollection( identifier, out var collection )
|| Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection )
? collection
: null;
// Check for the Yourself collection.
private static ModCollection? CheckYourself( ActorIdentifier identifier, GameObject* actor )
{
if( actor->ObjectIndex == 0
|| Cutscenes.GetParentIndex( actor->ObjectIndex ) == 0
|| identifier.Equals( Penumbra.Actors.GetCurrentPlayer() ) )
{
return Penumbra.CollectionManager.ByType( CollectionType.Yourself );
}
return null;
}
// Check special collections given the actor.
private static ModCollection? CollectionByAttributes( GameObject* actor )
{
if( !actor->IsCharacter() )
{
return null;
}
// Only handle human models.
var character = ( Character* )actor;
if( character->ModelCharaId >= 0 && character->ModelCharaId < ValidHumanModels.Count && ValidHumanModels[ character->ModelCharaId ] )
{
var bodyType = character->CustomizeData[2];
var collection = bodyType switch
{
3 => Penumbra.CollectionManager.ByType( CollectionType.NonPlayerElderly ),
4 => Penumbra.CollectionManager.ByType( CollectionType.NonPlayerChild ),
_ => null,
};
if( collection != null )
return collection;
var race = ( SubRace )character->CustomizeData[ 4 ];
var gender = ( Gender )( character->CustomizeData[ 1 ] + 1 );
var isNpc = actor->ObjectKind != ( byte )ObjectKind.Player;
var type = CollectionTypeExtensions.FromParts( race, gender, isNpc );
collection = Penumbra.CollectionManager.ByType( type );
collection ??= Penumbra.CollectionManager.ByType( CollectionTypeExtensions.FromParts( gender, isNpc ) );
return collection;
}
return null;
}
// Get the collection applying to the owner if it is available.
private static ModCollection? CheckOwnedCollection( ActorIdentifier identifier, GameObject* owner )
{
if( identifier.Type != IdentifierType.Owned || !Penumbra.Config.UseOwnerNameForCharacterCollection || owner == null )
{
return null;
}
var id = Penumbra.Actors.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue );
return CheckYourself( id, owner )
?? CollectionByAttributes( owner );
}
/// <summary>
/// Go through all ModelChara rows and return a bitfield of those that resolve to human models.
/// </summary>
private static BitArray GetValidHumanModels( DataManager gameData )
{
var sheet = gameData.GetExcelSheet< ModelChara >()!;
var ret = new BitArray( ( int )sheet.RowCount, false );
foreach( var (_, idx) in sheet.WithIndex().Where( p => p.Value.Type == ( byte )CharacterBase.ModelType.Human ) )
{
ret[ idx ] = true;
}
return ret;
}
}

View file

@ -1,220 +0,0 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Util;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
using static Penumbra.GameData.Enums.GenderRace;
namespace Penumbra.Interop.Resolver;
// State: 6.08 Hotfix.
// GetSlotEqpData seems to be the only function using the EQP table.
// It is only called by CheckSlotsForUnload (called by UpdateModels),
// SetupModelAttributes (called by UpdateModels and OnModelLoadComplete)
// and a unnamed function called by UpdateRender.
// It seems to be enough to change the EQP entries for UpdateModels.
// GetEqdpDataFor[Adults|Children|Other] seem to be the only functions using the EQDP tables.
// They are called by ResolveMdlPath, UpdateModels and SetupConnectorModelAttributes,
// which is called by SetupModelAttributes, which is called by OnModelLoadComplete and UpdateModels.
// It seems to be enough to change EQDP on UpdateModels and ResolveMDLPath.
// EST entries seem to be obtained by "44 8B C9 83 EA ?? 74", which is only called by
// ResolveSKLBPath, ResolveSKPPath, ResolvePHYBPath and indirectly by ResolvePAPPath.
// RSP height entries seem to be obtained by "E8 ?? ?? ?? ?? 48 8B 8E ?? ?? ?? ?? 44 8B CF"
// RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05"
// RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF"
// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create,
// ChangeCustomize and RspSetupCharacter, which is hooked here.
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter.
public unsafe partial class PathResolver
{
public class MetaState : IDisposable
{
public MetaState( IntPtr* humanVTable )
{
SignatureHelper.Initialise( this );
_onModelLoadCompleteHook = Hook< OnModelLoadCompleteDelegate >.FromAddress( humanVTable[ 58 ], OnModelLoadCompleteDetour );
}
public void Enable()
{
_getEqpIndirectHook.Enable();
_updateModelsHook.Enable();
_onModelLoadCompleteHook.Enable();
_setupVisorHook.Enable();
_rspSetupCharacterHook.Enable();
_changeCustomize.Enable();
}
public void Disable()
{
_getEqpIndirectHook.Disable();
_updateModelsHook.Disable();
_onModelLoadCompleteHook.Disable();
_setupVisorHook.Disable();
_rspSetupCharacterHook.Disable();
_changeCustomize.Disable();
}
public void Dispose()
{
_getEqpIndirectHook.Dispose();
_updateModelsHook.Dispose();
_onModelLoadCompleteHook.Dispose();
_setupVisorHook.Dispose();
_rspSetupCharacterHook.Dispose();
_changeCustomize.Dispose();
}
private delegate void OnModelLoadCompleteDelegate( IntPtr drawObject );
private readonly Hook< OnModelLoadCompleteDelegate > _onModelLoadCompleteHook;
private void OnModelLoadCompleteDetour( IntPtr drawObject )
{
var collection = GetResolveData( drawObject );
using var eqp = collection.ModCollection.TemporarilySetEqpFile();
using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true );
_onModelLoadCompleteHook.Original.Invoke( drawObject );
}
private delegate void UpdateModelDelegate( IntPtr drawObject );
[Signature( Sigs.UpdateModel, DetourName = nameof( UpdateModelsDetour ) )]
private readonly Hook< UpdateModelDelegate > _updateModelsHook = null!;
private void UpdateModelsDetour( IntPtr drawObject )
{
// Shortcut because this is called all the time.
// Same thing is checked at the beginning of the original function.
if( *( int* )( drawObject + Offsets.UpdateModelSkip ) == 0 )
{
return;
}
using var performance = Penumbra.Performance.Measure( PerformanceType.UpdateModels );
var collection = GetResolveData( drawObject );
using var eqp = collection.ModCollection.TemporarilySetEqpFile();
using var eqdp = ResolveEqdpData( collection.ModCollection, GetDrawObjectGenderRace( drawObject ), true, true );
_updateModelsHook.Original.Invoke( drawObject );
}
private static GenderRace GetDrawObjectGenderRace( IntPtr drawObject )
{
var draw = ( DrawObject* )drawObject;
if( draw->Object.GetObjectType() == ObjectType.CharacterBase )
{
var c = ( CharacterBase* )drawObject;
if( c->GetModelType() == CharacterBase.ModelType.Human )
{
return GetHumanGenderRace( drawObject );
}
}
return Unknown;
}
public static GenderRace GetHumanGenderRace( IntPtr human )
=> ( GenderRace )( ( Human* )human )->RaceSexId;
[Signature( Sigs.GetEqpIndirect, DetourName = nameof( GetEqpIndirectDetour ) )]
private readonly Hook< OnModelLoadCompleteDelegate > _getEqpIndirectHook = null!;
private void GetEqpIndirectDetour( IntPtr drawObject )
{
// Shortcut because this is also called all the time.
// Same thing is checked at the beginning of the original function.
if( ( *( byte* )( drawObject + Offsets.GetEqpIndirectSkip1 ) & 1 ) == 0 || *( ulong* )( drawObject + Offsets.GetEqpIndirectSkip2 ) == 0 )
{
return;
}
using var performance = Penumbra.Performance.Measure( PerformanceType.GetEqp );
var resolveData = GetResolveData( drawObject );
using var eqp = resolveData.ModCollection.TemporarilySetEqpFile();
_getEqpIndirectHook.Original( drawObject );
}
// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
// but it only applies a changed gmp file after a redraw for some reason.
private delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState );
[Signature( Sigs.SetupVisor, DetourName = nameof( SetupVisorDetour ) )]
private readonly Hook< SetupVisorDelegate > _setupVisorHook = null!;
private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.SetupVisor );
var resolveData = GetResolveData( drawObject );
using var gmp = resolveData.ModCollection.TemporarilySetGmpFile();
return _setupVisorHook.Original( drawObject, modelId, visorState );
}
// RSP
private delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 );
[Signature( Sigs.RspSetupCharacter, DetourName = nameof( RspSetupCharacterDetour ) )]
private readonly Hook< RspSetupCharacterDelegate > _rspSetupCharacterHook = null!;
private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 )
{
if( _inChangeCustomize )
{
_rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 );
}
else
{
using var performance = Penumbra.Performance.Measure( PerformanceType.SetupCharacter );
var resolveData = GetResolveData( drawObject );
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile();
_rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 );
}
}
// ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change.
private bool _inChangeCustomize;
private delegate bool ChangeCustomizeDelegate( IntPtr human, IntPtr data, byte skipEquipment );
[Signature( Sigs.ChangeCustomize, DetourName = nameof( ChangeCustomizeDetour ) )]
private readonly Hook< ChangeCustomizeDelegate > _changeCustomize = null!;
private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment )
{
using var performance = Penumbra.Performance.Measure( PerformanceType.ChangeCustomize );
_inChangeCustomize = true;
var resolveData = GetResolveData( human );
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile();
using var decals = new CharacterUtility.DecalReverter( Penumbra.ResourceService, resolveData.ModCollection, DrawObjectState.UsesDecal( 0, data ) );
var ret = _changeCustomize.Original( human, data, skipEquipment );
_inChangeCustomize = false;
return ret;
}
public static DisposableContainer ResolveEqdpData( ModCollection collection, GenderRace race, bool equipment, bool accessory )
{
var races = race.Dependencies();
if( races.Length == 0 )
{
return DisposableContainer.Empty;
}
var equipmentEnumerable = equipment
? races.Select( r => collection.TemporarilySetEqdpFile( r, false ) )
: Array.Empty< IDisposable? >().AsEnumerable();
var accessoryEnumerable = accessory
? races.Select( r => collection.TemporarilySetEqdpFile( r, true ) )
: Array.Empty< IDisposable? >().AsEnumerable();
return new DisposableContainer( equipmentEnumerable.Concat( accessoryEnumerable ) );
}
}
}

View file

@ -1,116 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.String;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class PathState : IDisposable
{
[Signature( Sigs.HumanVTable, ScanType = ScanType.StaticAddress )]
public readonly IntPtr* HumanVTable = null!;
[Signature( Sigs.WeaponVTable, ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _weaponVTable = null!;
[Signature( Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _demiHumanVTable = null!;
[Signature( Sigs.MonsterVTable, ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _monsterVTable = null!;
private readonly ResolverHooks _human;
private readonly ResolverHooks _weapon;
private readonly ResolverHooks _demiHuman;
private readonly ResolverHooks _monster;
// This map links files to their corresponding collection, if it is non-default.
private readonly ConcurrentDictionary< ByteString, ResolveData > _pathCollections = new();
private readonly ThreadLocal<ResolveData> _resolveData = new ThreadLocal<ResolveData>(() => ResolveData.Invalid, true);
public PathState( PathResolver parent )
{
SignatureHelper.Initialise( this );
_human = new ResolverHooks( parent, HumanVTable, ResolverHooks.Type.Human );
_weapon = new ResolverHooks( parent, _weaponVTable, ResolverHooks.Type.Weapon );
_demiHuman = new ResolverHooks( parent, _demiHumanVTable, ResolverHooks.Type.Other );
_monster = new ResolverHooks( parent, _monsterVTable, ResolverHooks.Type.Other );
}
public void Enable()
{
_human.Enable();
_weapon.Enable();
_demiHuman.Enable();
_monster.Enable();
}
public void Disable()
{
_human.Disable();
_weapon.Disable();
_demiHuman.Disable();
_monster.Disable();
}
public void Dispose()
{
_resolveData.Dispose();
_human.Dispose();
_weapon.Dispose();
_demiHuman.Dispose();
_monster.Dispose();
}
public int Count
=> _pathCollections.Count;
public IEnumerable< KeyValuePair< ByteString, ResolveData > > Paths
=> _pathCollections;
public bool TryGetValue( ByteString path, out ResolveData collection )
=> _pathCollections.TryGetValue( path, out collection );
public bool Consume( ByteString path, out ResolveData collection )
{
collection = _resolveData.IsValueCreated && _resolveData.Value.Valid ? _resolveData.Value : ResolveData.Invalid;
return _pathCollections.TryRemove(path, out collection);
}
// Just add or remove the resolved path.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
public IntPtr ResolvePath( IntPtr gameObject, ModCollection collection, IntPtr path )
{
if( path == IntPtr.Zero )
{
return path;
}
var gamePath = new ByteString( ( byte* )path );
SetCollection( gameObject, gamePath, collection );
_resolveData.Value = collection.ToResolveData(gameObject);
return path;
}
// Special handling for paths so that we do not store non-owned temporary strings in the dictionary.
public void SetCollection( IntPtr gameObject, ByteString path, ModCollection collection )
{
if( _pathCollections.ContainsKey( path ) || path.IsOwned )
{
_pathCollections[ path ] = collection.ToResolveData( gameObject );
}
else
{
_pathCollections[ path.Clone() ] = collection.ToResolveData( gameObject );
}
}
}
}

View file

@ -1,280 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Resolver;
public partial class PathResolver
{
public unsafe class ResolverHooks : IDisposable
{
public enum Type
{
Human,
Weapon,
Other,
}
private delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 );
private delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 );
private delegate IntPtr MaterialResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 );
private delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 );
private readonly Hook< GeneralResolveDelegate > _resolveDecalPathHook;
private readonly Hook< EidResolveDelegate > _resolveEidPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveImcPathHook;
private readonly Hook< MPapResolveDelegate > _resolveMPapPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveMdlPathHook;
private readonly Hook< MaterialResolveDelegate > _resolveMtrlPathHook;
private readonly Hook< MaterialResolveDelegate > _resolvePapPathHook;
private readonly Hook< GeneralResolveDelegate > _resolvePhybPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveSklbPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveSkpPathHook;
private readonly Hook< EidResolveDelegate > _resolveTmbPathHook;
private readonly Hook< MaterialResolveDelegate > _resolveVfxPathHook;
private readonly PathResolver _parent;
public ResolverHooks( PathResolver parent, IntPtr* vTable, Type type )
{
_parent = parent;
_resolveDecalPathHook = Create< GeneralResolveDelegate >( vTable[ 83 ], type, ResolveDecalWeapon, ResolveDecal );
_resolveEidPathHook = Create< EidResolveDelegate >( vTable[ 85 ], type, ResolveEidWeapon, ResolveEid );
_resolveImcPathHook = Create< GeneralResolveDelegate >( vTable[ 81 ], type, ResolveImcWeapon, ResolveImc );
_resolveMPapPathHook = Create< MPapResolveDelegate >( vTable[ 79 ], type, ResolveMPapWeapon, ResolveMPap );
_resolveMdlPathHook = Create< GeneralResolveDelegate >( vTable[ 73 ], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman );
_resolveMtrlPathHook = Create< MaterialResolveDelegate >( vTable[ 82 ], type, ResolveMtrlWeapon, ResolveMtrl );
_resolvePapPathHook = Create< MaterialResolveDelegate >( vTable[ 76 ], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman );
_resolvePhybPathHook = Create< GeneralResolveDelegate >( vTable[ 75 ], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman );
_resolveSklbPathHook = Create< GeneralResolveDelegate >( vTable[ 72 ], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman );
_resolveSkpPathHook = Create< GeneralResolveDelegate >( vTable[ 74 ], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman );
_resolveTmbPathHook = Create< EidResolveDelegate >( vTable[ 77 ], type, ResolveTmbWeapon, ResolveTmb );
_resolveVfxPathHook = Create< MaterialResolveDelegate >( vTable[ 84 ], type, ResolveVfxWeapon, ResolveVfx );
}
public void Enable()
{
_resolveDecalPathHook.Enable();
_resolveEidPathHook.Enable();
_resolveImcPathHook.Enable();
_resolveMPapPathHook.Enable();
_resolveMdlPathHook.Enable();
_resolveMtrlPathHook.Enable();
_resolvePapPathHook.Enable();
_resolvePhybPathHook.Enable();
_resolveSklbPathHook.Enable();
_resolveSkpPathHook.Enable();
_resolveTmbPathHook.Enable();
_resolveVfxPathHook.Enable();
}
public void Disable()
{
_resolveDecalPathHook.Disable();
_resolveEidPathHook.Disable();
_resolveImcPathHook.Disable();
_resolveMPapPathHook.Disable();
_resolveMdlPathHook.Disable();
_resolveMtrlPathHook.Disable();
_resolvePapPathHook.Disable();
_resolvePhybPathHook.Disable();
_resolveSklbPathHook.Disable();
_resolveSkpPathHook.Disable();
_resolveTmbPathHook.Disable();
_resolveVfxPathHook.Disable();
}
public void Dispose()
{
_resolveDecalPathHook.Dispose();
_resolveEidPathHook.Dispose();
_resolveImcPathHook.Dispose();
_resolveMPapPathHook.Dispose();
_resolveMdlPathHook.Dispose();
_resolveMtrlPathHook.Dispose();
_resolvePapPathHook.Dispose();
_resolvePhybPathHook.Dispose();
_resolveSklbPathHook.Dispose();
_resolveSkpPathHook.Dispose();
_resolveTmbPathHook.Dispose();
_resolveVfxPathHook.Dispose();
}
private IntPtr ResolveDecal( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveEid( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveImc( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMPap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveMtrl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePhyb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSklb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSkp( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveTmb( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveVfx( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
{
DisposableContainer Get()
{
if( modelType > 9 )
{
return DisposableContainer.Empty;
}
var data = GetResolveData( drawObject );
return MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace( drawObject ), modelType < 5, modelType > 4);
}
using var eqdp = Get();
return ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) );
}
private IntPtr ResolvePapHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
{
using var est = GetEstChanges( drawObject );
return ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
}
private IntPtr ResolvePhybHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = GetEstChanges( drawObject );
return ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSklbHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = GetEstChanges( drawObject );
return ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSkpHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = GetEstChanges( drawObject );
return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private static DisposableContainer GetEstChanges( IntPtr drawObject )
{
var data = GetResolveData( drawObject );
return new DisposableContainer( data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Face ),
data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Body ),
data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Hair ),
data.ModCollection.TemporarilySetEstFile( EstManipulation.EstType.Head ) );
}
private IntPtr ResolveDecalWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveEidWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveImcWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMPapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolveWeaponPath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolveWeaponPath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveMtrlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePhybWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSklbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSkpWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveTmbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveVfxWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other, T human ) where T : Delegate
{
var del = type switch
{
Type.Human => human,
Type.Weapon => weapon,
_ => other,
};
return Hook< T >.FromAddress( address, del );
}
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other ) where T : Delegate
=> Create( address, type, weapon, other, other );
// Implementation
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolvePath( IntPtr drawObject, IntPtr path )
=> _parent._paths.ResolvePath( ( IntPtr? )FindParent( drawObject, out _ ) ?? IntPtr.Zero,
FindParent( drawObject, out var collection ) == null
? Penumbra.CollectionManager.Default
: collection.ModCollection, path );
// Weapons have the characters DrawObject as a parent,
// but that may not be set yet when creating a new object, so we have to do the same detour
// as for Human DrawObjects that are just being created.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolveWeaponPath( IntPtr drawObject, IntPtr path )
{
var parent = FindParent( drawObject, out var collection );
if( parent != null )
{
return _parent._paths.ResolvePath( ( IntPtr )parent, collection.ModCollection, path );
}
var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject;
var parentCollection = _drawObjects.CheckParentDrawObject( drawObject, parentObject );
if( parentCollection.Valid )
{
return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path );
}
parent = FindParent( parentObject, out collection );
return _parent._paths.ResolvePath( ( IntPtr? )parent ?? IntPtr.Zero, parent == null
? Penumbra.CollectionManager.Default
: collection.ModCollection, path );
}
}
}

View file

@ -1,194 +0,0 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
// Materials and avfx do contain their own paths to textures and shader packages or atex respectively.
// Those are loaded synchronously.
// Thus, we need to ensure the correct files are loaded when a material is loaded.
public class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<IntPtr, ResolveData>>
{
private readonly ResourceLoader _loader;
private readonly GameEventManager _events;
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
private readonly ConcurrentDictionary<IntPtr, ResolveData> _subFileCollection = new();
public SubfileHelper(ResourceLoader loader, GameEventManager events)
{
SignatureHelper.Initialise(this);
_loader = loader;
_events = events;
}
// Check specifically for shpk and tex files whether we are currently in a material load.
public bool HandleSubFiles(ResourceType type, out ResolveData collection)
{
switch (type)
{
case ResourceType.Tex when _mtrlData.Value.Valid:
case ResourceType.Shpk when _mtrlData.Value.Valid:
collection = _mtrlData.Value;
return true;
case ResourceType.Scd when _avfxData.Value.Valid:
collection = _avfxData.Value;
return true;
case ResourceType.Atex when _avfxData.Value.Valid:
collection = _avfxData.Value;
return true;
}
collection = ResolveData.Invalid;
return false;
}
// Materials need to be set per collection so they can load their textures independently from each other.
public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved,
out (FullPath?, ResolveData) data)
{
if (nonDefault)
switch (type)
{
case ResourceType.Mtrl:
case ResourceType.Avfx:
var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}");
data = (fullPath, resolveData);
return;
}
data = (resolved, resolveData);
}
public void Enable()
{
_loadMtrlShpkHook.Enable();
_loadMtrlTexHook.Enable();
_apricotResourceLoadHook.Enable();
_loader.ResourceLoaded += SubfileContainerRequested;
_events.ResourceHandleDestructor += ResourceDestroyed;
}
public void Disable()
{
_loadMtrlShpkHook.Disable();
_loadMtrlTexHook.Disable();
_apricotResourceLoadHook.Disable();
_loader.ResourceLoaded -= SubfileContainerRequested;
_events.ResourceHandleDestructor -= ResourceDestroyed;
}
public void Dispose()
{
Disable();
_loadMtrlShpkHook.Dispose();
_loadMtrlTexHook.Dispose();
_apricotResourceLoadHook.Dispose();
}
private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData)
{
switch (handle->FileType)
{
case ResourceType.Mtrl:
case ResourceType.Avfx:
if (handle->FileSize == 0)
_subFileCollection[(nint)handle] = resolveData;
break;
}
}
private void ResourceDestroyed(ResourceHandle* handle)
=> _subFileCollection.TryRemove((IntPtr)handle, out _);
private delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle);
[Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
private readonly Hook<LoadMtrlFilesDelegate> _loadMtrlTexHook = null!;
private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle)
{
using var performance = Penumbra.Performance.Measure(PerformanceType.LoadTextures);
var old = _mtrlData.Value;
_mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
var ret = _loadMtrlTexHook.Original(mtrlResourceHandle);
_mtrlData.Value = old;
return ret;
}
[Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))]
private readonly Hook<LoadMtrlFilesDelegate> _loadMtrlShpkHook = null!;
private byte LoadMtrlShpkDetour(IntPtr mtrlResourceHandle)
{
using var performance = Penumbra.Performance.Measure(PerformanceType.LoadShaders);
var old = _mtrlData.Value;
_mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle);
_mtrlData.Value = old;
return ret;
}
private ResolveData LoadFileHelper(IntPtr resourceHandle)
{
if (resourceHandle == IntPtr.Zero)
return ResolveData.Invalid;
return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid;
}
private delegate byte ApricotResourceLoadDelegate(IntPtr handle, IntPtr unk1, byte unk2);
[Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))]
private readonly Hook<ApricotResourceLoadDelegate> _apricotResourceLoadHook = null!;
private byte ApricotResourceLoadDetour(IntPtr handle, IntPtr unk1, byte unk2)
{
using var performance = Penumbra.Performance.Measure(PerformanceType.LoadApricotResources);
var old = _avfxData.Value;
_avfxData.Value = LoadFileHelper(handle);
var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2);
_avfxData.Value = old;
return ret;
}
public IEnumerator<KeyValuePair<IntPtr, ResolveData>> GetEnumerator()
=> _subFileCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _subFileCollection.Count;
internal ResolveData MtrlData
=> _mtrlData.Value;
internal ResolveData AvfxData
=> _avfxData.Value;
}
}

View file

@ -1,206 +1,140 @@
using System; using System;
using System.Collections; using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Dalamud.Game.ClientState; using Penumbra.Api;
using Dalamud.Utility.Signatures; using Penumbra.Collections;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.GameData.Enums;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Penumbra.Interop.Loader;
using Penumbra.Collections; using Penumbra.Interop.Structs;
using Penumbra.GameData.Enums; using Penumbra.String;
using Penumbra.Interop.Loader; using Penumbra.String.Classes;
using Penumbra.Interop.Services; using Penumbra.Util;
using Penumbra.Services;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.Resolver;
//public class PathResolver2 : IDisposable
//{
// public readonly CutsceneService Cutscenes;
// public readonly IdentifiedCollectionCache Identified;
//
// public PathResolver(StartTracker timer, CutsceneService cutscenes, IdentifiedCollectionCache identified)
// {
// using var t = timer.Measure(StartTimeType.PathResolver);
// Cutscenes = cutscenes;
// Identified = identified;
// }
//}
namespace Penumbra.Interop.Resolver;
// The Path Resolver handles character collections.
// It will hook any path resolving functions for humans, public class PathResolver : IDisposable
// as well as DrawObject creation. {
// It links draw objects to actors, and actors to character collections, private readonly PerformanceTracker _performance;
// to resolve paths for character collections. private readonly Configuration _config;
public partial class PathResolver : IDisposable private readonly ModCollection.Manager _collectionManager;
{ private readonly TempCollectionManager _tempCollections;
public bool Enabled { get; private set; } private readonly ResourceLoader _loader;
private readonly CommunicatorService _communicator; private readonly AnimationHookService _animationHookService;
private readonly ResourceLoader _loader; private readonly SubfileHelper _subfileHelper;
private static readonly CutsceneService Cutscenes = new(DalamudServices.SObjects, Penumbra.GameEvents); // TODO private readonly PathState _pathState;
private static DrawObjectState _drawObjects = null!; // TODO private readonly MetaState _metaState;
private static readonly BitArray ValidHumanModels;
internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
private readonly AnimationState _animations; TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
private readonly PathState _paths; PathState pathState, MetaState metaState)
private readonly MetaState _meta; {
private readonly SubfileHelper _subFiles; _performance = performance;
_config = config;
static PathResolver() _collectionManager = collectionManager;
=> ValidHumanModels = GetValidHumanModels(DalamudServices.SGameData); _tempCollections = tempCollections;
_animationHookService = animationHookService;
public unsafe PathResolver(IdentifiedCollectionCache cache, StartTracker timer, ClientState clientState, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) _subfileHelper = subfileHelper;
{ _pathState = pathState;
using var tApi = timer.Measure(StartTimeType.PathResolver); _metaState = metaState;
_communicator = communicator; _loader = loader;
IdentifiedCache = cache; _loader.ResolvePath = ResolvePath;
SignatureHelper.Initialise(this); _loader.FileLoaded += ImcLoadResource;
_drawObjects = new DrawObjectState(_communicator); }
_loader = loader;
_animations = new AnimationState(_drawObjects); /// <summary> Obtain a temporary or permanent collection by name. </summary>
_paths = new PathState(this); public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
_meta = new MetaState(_paths.HumanVTable); => _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection);
_subFiles = new SubfileHelper(_loader, Penumbra.GameEvents);
Enable(); /// <summary> Try to resolve the given game path to the replaced path. </summary>
} public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType)
{
// The modified resolver that handles game path resolving. // Check if mods are enabled or if we are in a inc-ref at 0 reference count situation.
public (FullPath?, ResolveData) CharacterResolver(Utf8GamePath gamePath, ResourceType type) if (!_config.EnableMods)
{ return (null, ResolveData.Invalid);
using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver);
// Check if the path was marked for a specific collection, path = path.ToLower();
// or if it is a file loaded by a material, and if we are currently in a material load, return category switch
// or if it is a face decal path and the current mod collection is set. {
// If not use the default collection. // Only Interface collection.
// We can remove paths after they have actually been loaded. ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path),
// A potential next request will add the path anew. _collectionManager.Interface.ToResolveData()),
var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData) // Never allow changing scripts.
|| _paths.Consume(gamePath.Path, out resolveData) ResourceCategory.UiScript => (null, ResolveData.Invalid),
|| _animations.HandleFiles(type, gamePath, out resolveData) ResourceCategory.GameScript => (null, ResolveData.Invalid),
|| _drawObjects.HandleDecalFile(type, gamePath, out resolveData); // Use actual resolving.
if (!nonDefault || !resolveData.Valid) ResourceCategory.Chara => Resolve(path, resourceType),
resolveData = Penumbra.CollectionManager.Default.ToResolveData(); ResourceCategory.Shader => Resolve(path, resourceType),
ResourceCategory.Vfx => Resolve(path, resourceType),
// Resolve using character/default collection first, otherwise forced, as usual. ResourceCategory.Sound => Resolve(path, resourceType),
var resolved = resolveData.ModCollection.ResolvePath(gamePath); // None of these files are ever associated with specific characters,
// always use the default resolver for now.
// Since mtrl files load their files separately, we need to add the new, resolved path ResourceCategory.Common => DefaultResolver(path),
// so that the functions loading tex and shpk can find that path and use its collection. ResourceCategory.BgCommon => DefaultResolver(path),
// We also need to handle defaulted materials against a non-default collection. ResourceCategory.Bg => DefaultResolver(path),
var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; ResourceCategory.Cut => DefaultResolver(path),
SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair); ResourceCategory.Exd => DefaultResolver(path),
return pair; ResourceCategory.Music => DefaultResolver(path),
} _ => DefaultResolver(path),
};
public void Enable() }
{
if (Enabled) public (FullPath?, ResolveData) Resolve(Utf8GamePath gamePath, ResourceType type)
return; {
using var performance = _performance.Measure(PerformanceType.CharacterResolver);
Enabled = true; // Check if the path was marked for a specific collection,
Cutscenes.Enable(); // or if it is a file loaded by a material, and if we are currently in a material load,
_drawObjects.Enable(); // or if it is a face decal path and the current mod collection is set.
IdentifiedCache.Enable(); // If not use the default collection.
_animations.Enable(); // We can remove paths after they have actually been loaded.
_paths.Enable(); // A potential next request will add the path anew.
_meta.Enable(); var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData)
_subFiles.Enable(); || _pathState.Consume(gamePath.Path, out resolveData)
|| _animationHookService.HandleFiles(type, gamePath, out resolveData)
Penumbra.Log.Debug("Character Path Resolver enabled."); || _metaState.HandleDecalFile(type, gamePath, out resolveData);
} if (!nonDefault || !resolveData.Valid)
resolveData = _collectionManager.Default.ToResolveData();
public void Disable()
{ // Resolve using character/default collection first, otherwise forced, as usual.
if (!Enabled) var resolved = resolveData.ModCollection.ResolvePath(gamePath);
return;
// Since mtrl files load their files separately, we need to add the new, resolved path
Enabled = false; // so that the functions loading tex and shpk can find that path and use its collection.
_animations.Disable(); // We also need to handle defaulted materials against a non-default collection.
_drawObjects.Disable(); var path = resolved == null ? gamePath.Path : resolved.Value.InternalName;
Cutscenes.Disable(); SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out var pair);
IdentifiedCache.Disable(); return pair;
_paths.Disable(); }
_meta.Disable();
_subFiles.Disable(); public unsafe void Dispose()
{
Penumbra.Log.Debug("Character Path Resolver disabled."); _loader.ResetResolvePath();
} _loader.FileLoaded -= ImcLoadResource;
}
public void Dispose()
{ /// <summary> Use the default method of path replacement. </summary>
Disable(); private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path)
_paths.Dispose(); {
_animations.Dispose(); var resolved = _collectionManager.Default.ResolvePath(path);
_drawObjects.Dispose(); return (resolved, _collectionManager.Default.ToResolveData());
Cutscenes.Dispose(); }
IdentifiedCache.Dispose();
_meta.Dispose(); /// <summary> After loading an IMC file, replace its contents with the modded IMC file. </summary>
_subFiles.Dispose(); private unsafe void ImcLoadResource(ResourceHandle* resource, ByteString path, bool returnValue, bool custom, ByteString additionalData)
} {
if (resource->FileType != ResourceType.Imc)
public static unsafe (IntPtr, ResolveData) IdentifyDrawObject(IntPtr drawObject) return;
{
var parent = FindParent(drawObject, out var resolveData); var lastUnderscore = additionalData.LastIndexOf((byte)'_');
return ((IntPtr)parent, resolveData); var name = lastUnderscore == -1 ? additionalData.ToString() : additionalData.Substring(0, lastUnderscore).ToString();
} if (Utf8GamePath.FromByteString(path, out var gamePath)
&& CollectionByName(name, out var collection)
public int CutsceneActor(int idx) && collection.HasCache
=> Cutscenes.GetParentIndex(idx); && collection.GetImcFile(gamePath, out var file))
{
// Use the stored information to find the GameObject and Collection linked to a DrawObject. file.Replace(resource);
public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData) Penumbra.Log.Verbose(
{ $"[ResourceLoader] Loaded {gamePath} from file and replaced with IMC from collection {collection.AnonymizedName}.");
if (_drawObjects.TryGetValue(drawObject, out var data, out var gameObject)) }
{ }
resolveData = data.Item1; }
return gameObject;
}
if (_drawObjects.LastGameObject != null
&& (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject))
{
resolveData = IdentifyCollection(_drawObjects.LastGameObject, true);
return _drawObjects.LastGameObject;
}
resolveData = IdentifyCollection(null, true);
return null;
}
private static unsafe ResolveData GetResolveData(IntPtr drawObject)
{
var _ = FindParent(drawObject, out var resolveData);
return resolveData;
}
internal IEnumerable<KeyValuePair<ByteString, ResolveData>> PathCollections
=> _paths.Paths;
internal IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjectMap
=> _drawObjects.DrawObjects;
internal IEnumerable<KeyValuePair<int, global::Dalamud.Game.ClientState.Objects.Types.GameObject>> CutsceneActors
=> Cutscenes.Actors;
internal IEnumerable<KeyValuePair<IntPtr, ResolveData>> ResourceCollections
=> _subFiles;
internal int SubfileCount
=> _subFiles.Count;
internal ResolveData CurrentMtrlData
=> _subFiles.MtrlData;
internal ResolveData CurrentAvfxData
=> _subFiles.AvfxData;
internal ResolveData LastGameObjectData
=> _drawObjects.LastCreatedCollection;
internal unsafe nint LastGameObject
=> (nint)_drawObjects.LastGameObject;
}

View file

@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.String;
namespace Penumbra.Interop.Resolver;
public unsafe class PathState : IDisposable
{
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _humanVTable = null!;
[Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _weaponVTable = null!;
[Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _demiHumanVTable = null!;
[Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _monsterVTable = null!;
public readonly CollectionResolver CollectionResolver;
private readonly ResolvePathHooks _human;
private readonly ResolvePathHooks _weapon;
private readonly ResolvePathHooks _demiHuman;
private readonly ResolvePathHooks _monster;
private readonly ThreadLocal<ResolveData> _resolveData = new(() => ResolveData.Invalid, true);
public IList<ResolveData> CurrentData
=> _resolveData.Values;
public PathState(CollectionResolver collectionResolver)
{
SignatureHelper.Initialise(this);
CollectionResolver = collectionResolver;
_human = new ResolvePathHooks(this, _humanVTable, ResolvePathHooks.Type.Human);
_weapon = new ResolvePathHooks(this, _weaponVTable, ResolvePathHooks.Type.Weapon);
_demiHuman = new ResolvePathHooks(this, _demiHumanVTable, ResolvePathHooks.Type.Other);
_monster = new ResolvePathHooks(this, _monsterVTable, ResolvePathHooks.Type.Other);
_human.Enable();
_weapon.Enable();
_demiHuman.Enable();
_monster.Enable();
}
public void Dispose()
{
_resolveData.Dispose();
_human.Dispose();
_weapon.Dispose();
_demiHuman.Dispose();
_monster.Dispose();
}
public bool Consume(ByteString path, out ResolveData collection)
{
if (_resolveData.IsValueCreated)
{
collection = _resolveData.Value;
_resolveData.Value = ResolveData.Invalid;
return collection.Valid;
}
collection = ResolveData.Invalid;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public nint ResolvePath(nint gameObject, ModCollection collection, nint path)
{
if (path == nint.Zero)
return path;
_resolveData.Value = collection.ToResolveData(gameObject);
return path;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public nint ResolvePath(ResolveData data, nint path)
{
if (path == nint.Zero)
return path;
_resolveData.Value = data;
return path;
}
}

View file

@ -0,0 +1,249 @@
using System;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Resolver;
public unsafe class ResolvePathHooks : IDisposable
{
public enum Type
{
Human,
Weapon,
Other,
}
private delegate nint GeneralResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4);
private delegate nint MPapResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4, uint unk5);
private delegate nint MaterialResolveDelegate(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5);
private delegate nint EidResolveDelegate(nint drawObject, nint path, nint unk3);
private readonly Hook<GeneralResolveDelegate> _resolveDecalPathHook;
private readonly Hook<EidResolveDelegate> _resolveEidPathHook;
private readonly Hook<GeneralResolveDelegate> _resolveImcPathHook;
private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook;
private readonly Hook<GeneralResolveDelegate> _resolveMdlPathHook;
private readonly Hook<MaterialResolveDelegate> _resolveMtrlPathHook;
private readonly Hook<MaterialResolveDelegate> _resolvePapPathHook;
private readonly Hook<GeneralResolveDelegate> _resolvePhybPathHook;
private readonly Hook<GeneralResolveDelegate> _resolveSklbPathHook;
private readonly Hook<GeneralResolveDelegate> _resolveSkpPathHook;
private readonly Hook<EidResolveDelegate> _resolveTmbPathHook;
private readonly Hook<MaterialResolveDelegate> _resolveVfxPathHook;
private readonly PathState _parent;
public ResolvePathHooks(PathState parent, nint* vTable, Type type)
{
_parent = parent;
_resolveDecalPathHook = Create<GeneralResolveDelegate>(vTable[83], type, ResolveDecalWeapon, ResolveDecal);
_resolveEidPathHook = Create<EidResolveDelegate>(vTable[85], type, ResolveEidWeapon, ResolveEid);
_resolveImcPathHook = Create<GeneralResolveDelegate>(vTable[81], type, ResolveImcWeapon, ResolveImc);
_resolveMPapPathHook = Create<MPapResolveDelegate>(vTable[79], type, ResolveMPapWeapon, ResolveMPap);
_resolveMdlPathHook = Create<GeneralResolveDelegate>(vTable[73], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman);
_resolveMtrlPathHook = Create<MaterialResolveDelegate>(vTable[82], type, ResolveMtrlWeapon, ResolveMtrl);
_resolvePapPathHook = Create<MaterialResolveDelegate>(vTable[76], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman);
_resolvePhybPathHook = Create<GeneralResolveDelegate>(vTable[75], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman);
_resolveSklbPathHook = Create<GeneralResolveDelegate>(vTable[72], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman);
_resolveSkpPathHook = Create<GeneralResolveDelegate>(vTable[74], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman);
_resolveTmbPathHook = Create<EidResolveDelegate>(vTable[77], type, ResolveTmbWeapon, ResolveTmb);
_resolveVfxPathHook = Create<MaterialResolveDelegate>(vTable[84], type, ResolveVfxWeapon, ResolveVfx);
}
public void Enable()
{
_resolveDecalPathHook.Enable();
_resolveEidPathHook.Enable();
_resolveImcPathHook.Enable();
_resolveMPapPathHook.Enable();
_resolveMdlPathHook.Enable();
_resolveMtrlPathHook.Enable();
_resolvePapPathHook.Enable();
_resolvePhybPathHook.Enable();
_resolveSklbPathHook.Enable();
_resolveSkpPathHook.Enable();
_resolveTmbPathHook.Enable();
_resolveVfxPathHook.Enable();
}
public void Disable()
{
_resolveDecalPathHook.Disable();
_resolveEidPathHook.Disable();
_resolveImcPathHook.Disable();
_resolveMPapPathHook.Disable();
_resolveMdlPathHook.Disable();
_resolveMtrlPathHook.Disable();
_resolvePapPathHook.Disable();
_resolvePhybPathHook.Disable();
_resolveSklbPathHook.Disable();
_resolveSkpPathHook.Disable();
_resolveTmbPathHook.Disable();
_resolveVfxPathHook.Disable();
}
public void Dispose()
{
_resolveDecalPathHook.Dispose();
_resolveEidPathHook.Dispose();
_resolveImcPathHook.Dispose();
_resolveMPapPathHook.Dispose();
_resolveMdlPathHook.Dispose();
_resolveMtrlPathHook.Dispose();
_resolvePapPathHook.Dispose();
_resolvePhybPathHook.Dispose();
_resolveSklbPathHook.Dispose();
_resolveSkpPathHook.Dispose();
_resolveTmbPathHook.Dispose();
_resolveVfxPathHook.Dispose();
}
private nint ResolveDecal(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveEid(nint drawObject, nint path, nint unk3)
=> ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, path, unk3));
private nint ResolveImc(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveMPap(nint drawObject, nint path, nint unk3, uint unk4, uint unk5)
=> ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolveMdl(nint drawObject, nint path, nint unk3, uint modelType)
=> ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType));
private nint ResolveMtrl(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
=> ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolvePap(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolvePhyb(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveSklb(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveSkp(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveTmb(nint drawObject, nint path, nint unk3)
=> ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, path, unk3));
private nint ResolveVfx(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
=> ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolveMdlHuman(nint drawObject, nint path, nint unk3, uint modelType)
{
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
using var eqdp = modelType > 9
? DisposableContainer.Empty
: MetaState.ResolveEqdpData(data.ModCollection, MetaState.GetHumanGenderRace(drawObject), modelType < 5, modelType > 4);
return ResolvePath(data, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType));
}
private nint ResolvePapHuman(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5));
}
private nint ResolvePhybHuman(nint drawObject, nint path, nint unk3, uint unk4)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4));
}
private nint ResolveSklbHuman(nint drawObject, nint path, nint unk3, uint unk4)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4));
}
private nint ResolveSkpHuman(nint drawObject, nint path, nint unk3, uint unk4)
{
using var est = GetEstChanges(drawObject, out var data);
return ResolvePath(data, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4));
}
private DisposableContainer GetEstChanges(nint drawObject, out ResolveData data)
{
data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
return new DisposableContainer(data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Face),
data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Body),
data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Hair),
data.ModCollection.TemporarilySetEstFile(EstManipulation.EstType.Head));
}
private nint ResolveDecalWeapon(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveDecalPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveEidWeapon(nint drawObject, nint path, nint unk3)
=> ResolvePath(drawObject, _resolveEidPathHook.Original(drawObject, path, unk3));
private nint ResolveImcWeapon(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveImcPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveMPapWeapon(nint drawObject, nint path, nint unk3, uint unk4, uint unk5)
=> ResolvePath(drawObject, _resolveMPapPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolveMdlWeapon(nint drawObject, nint path, nint unk3, uint modelType)
=> ResolvePath(drawObject, _resolveMdlPathHook.Original(drawObject, path, unk3, modelType));
private nint ResolveMtrlWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
=> ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolvePapWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, path, unk3, unk4, unk5));
private nint ResolvePhybWeapon(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveSklbWeapon(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveSkpWeapon(nint drawObject, nint path, nint unk3, uint unk4)
=> ResolvePath(drawObject, _resolveSkpPathHook.Original(drawObject, path, unk3, unk4));
private nint ResolveTmbWeapon(nint drawObject, nint path, nint unk3)
=> ResolvePath(drawObject, _resolveTmbPathHook.Original(drawObject, path, unk3));
private nint ResolveVfxWeapon(nint drawObject, nint path, nint unk3, uint unk4, ulong unk5)
=> ResolvePath(drawObject, _resolveVfxPathHook.Original(drawObject, path, unk3, unk4, unk5));
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static Hook<T> Create<T>(nint address, Type type, T weapon, T other, T human) where T : Delegate
{
var del = type switch
{
Type.Human => human,
Type.Weapon => weapon,
_ => other,
};
return Hook<T>.FromAddress(address, del);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static Hook<T> Create<T>(nint address, Type type, T weapon, T other) where T : Delegate
=> Create(address, type, weapon, other, other);
// Implementation
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private nint ResolvePath(nint drawObject, nint path)
{
var data = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
return ResolvePath(data, path);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private nint ResolvePath(ResolveData data, nint path)
=> _parent.ResolvePath(data, path);
}

View file

@ -0,0 +1,182 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Interop.Resolver;
/// <summary>
/// Materials and avfx do contain their own paths to textures and shader packages or atex respectively.
/// Those are loaded synchronously.
/// Thus, we need to ensure the correct files are loaded when a material is loaded.
/// </summary>
public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
{
private readonly PerformanceTracker _performance;
private readonly ResourceLoader _loader;
private readonly GameEventManager _events;
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
private readonly ConcurrentDictionary<nint, ResolveData> _subFileCollection = new();
public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events)
{
SignatureHelper.Initialise(this);
_performance = performance;
_loader = loader;
_events = events;
_loadMtrlShpkHook.Enable();
_loadMtrlTexHook.Enable();
_apricotResourceLoadHook.Enable();
_loader.ResourceLoaded += SubfileContainerRequested;
_events.ResourceHandleDestructor += ResourceDestroyed;
}
public IEnumerator<KeyValuePair<nint, ResolveData>> GetEnumerator()
=> _subFileCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _subFileCollection.Count;
public ResolveData MtrlData
=> _mtrlData.IsValueCreated ? _mtrlData.Value : ResolveData.Invalid;
public ResolveData AvfxData
=> _avfxData.IsValueCreated ? _avfxData.Value : ResolveData.Invalid;
/// <summary>
/// Check specifically for shpk and tex files whether we are currently in a material load,
/// and for scd and atex files whether we are in an avfx load. </summary>
public bool HandleSubFiles(ResourceType type, out ResolveData collection)
{
switch (type)
{
case ResourceType.Tex when _mtrlData.Value.Valid:
case ResourceType.Shpk when _mtrlData.Value.Valid:
collection = _mtrlData.Value;
return true;
case ResourceType.Scd when _avfxData.Value.Valid:
case ResourceType.Atex when _avfxData.Value.Valid:
collection = _avfxData.Value;
return true;
}
collection = ResolveData.Invalid;
return false;
}
/// <summary> Materials and AVFX need to be set per collection so they can load their textures independently from each other. </summary>
public static void HandleCollection(ResolveData resolveData, ByteString path, bool nonDefault, ResourceType type, FullPath? resolved,
out (FullPath?, ResolveData) data)
{
if (nonDefault)
switch (type)
{
case ResourceType.Mtrl:
case ResourceType.Avfx:
var fullPath = new FullPath($"|{resolveData.ModCollection.Name}_{resolveData.ModCollection.ChangeCounter}|{path}");
data = (fullPath, resolveData);
return;
}
data = (resolved, resolveData);
}
public void Dispose()
{
_loader.ResourceLoaded -= SubfileContainerRequested;
_events.ResourceHandleDestructor -= ResourceDestroyed;
_loadMtrlShpkHook.Dispose();
_loadMtrlTexHook.Dispose();
_apricotResourceLoadHook.Dispose();
}
private void SubfileContainerRequested(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
ResolveData resolveData)
{
switch (handle->FileType)
{
case ResourceType.Mtrl:
case ResourceType.Avfx:
if (handle->FileSize == 0)
_subFileCollection[(nint)handle] = resolveData;
break;
}
}
private void ResourceDestroyed(ResourceHandle* handle)
=> _subFileCollection.TryRemove((nint)handle, out _);
private delegate byte LoadMtrlFilesDelegate(nint mtrlResourceHandle);
[Signature(Sigs.LoadMtrlTex, DetourName = nameof(LoadMtrlTexDetour))]
private readonly Hook<LoadMtrlFilesDelegate> _loadMtrlTexHook = null!;
private byte LoadMtrlTexDetour(nint mtrlResourceHandle)
{
using var performance = _performance.Measure(PerformanceType.LoadTextures);
var last = _mtrlData.Value;
_mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
var ret = _loadMtrlTexHook.Original(mtrlResourceHandle);
_mtrlData.Value = last;
return ret;
}
[Signature(Sigs.LoadMtrlShpk, DetourName = nameof(LoadMtrlShpkDetour))]
private readonly Hook<LoadMtrlFilesDelegate> _loadMtrlShpkHook = null!;
private byte LoadMtrlShpkDetour(nint mtrlResourceHandle)
{
using var performance = _performance.Measure(PerformanceType.LoadShaders);
var last = _mtrlData.Value;
_mtrlData.Value = LoadFileHelper(mtrlResourceHandle);
var ret = _loadMtrlShpkHook.Original(mtrlResourceHandle);
_mtrlData.Value = last;
return ret;
}
private ResolveData LoadFileHelper(nint resourceHandle)
{
if (resourceHandle == nint.Zero)
return ResolveData.Invalid;
return _subFileCollection.TryGetValue(resourceHandle, out var c) ? c : ResolveData.Invalid;
}
private delegate byte ApricotResourceLoadDelegate(nint handle, nint unk1, byte unk2);
[Signature(Sigs.ApricotResourceLoad, DetourName = nameof(ApricotResourceLoadDetour))]
private readonly Hook<ApricotResourceLoadDelegate> _apricotResourceLoadHook = null!;
private byte ApricotResourceLoadDetour(nint handle, nint unk1, byte unk2)
{
using var performance = _performance.Measure(PerformanceType.LoadApricotResources);
var last = _avfxData.Value;
_avfxData.Value = LoadFileHelper(handle);
var ret = _apricotResourceLoadHook.Original(handle, unk1, unk2);
_avfxData.Value = last;
return ret;
}
}

View file

@ -4,21 +4,32 @@ using Penumbra.GameData;
using System; using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Log;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Services; namespace Penumbra.Interop.Services;
public unsafe class GameEventManager : IDisposable public unsafe class GameEventManager : IDisposable
{ {
private const string Prefix = $"[{nameof(GameEventManager)}]"; private const string Prefix = $"[{nameof(GameEventManager)}]";
public event CharacterDestructorEvent? CharacterDestructor;
public event CopyCharacterEvent? CopyCharacter;
public event ResourceHandleDestructorEvent? ResourceHandleDestructor;
public event CreatingCharacterBaseEvent? CreatingCharacterBase;
public event CharacterBaseCreatedEvent? CharacterBaseCreated;
public event CharacterBaseDestructorEvent? CharacterBaseDestructor;
public event WeaponReloadingEvent? WeaponReloading;
public event WeaponReloadedEvent? WeaponReloaded;
public GameEventManager() public GameEventManager()
{ {
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_characterDtorHook.Enable(); _characterDtorHook.Enable();
_copyCharacterHook.Enable(); _copyCharacterHook.Enable();
_resourceHandleDestructorHook.Enable(); _resourceHandleDestructorHook.Enable();
_characterBaseCreateHook.Enable();
_characterBaseDestructorHook.Enable();
_weaponReloadHook.Enable();
Penumbra.Log.Verbose($"{Prefix} Created."); Penumbra.Log.Verbose($"{Prefix} Created.");
} }
@ -27,6 +38,9 @@ public unsafe class GameEventManager : IDisposable
_characterDtorHook.Dispose(); _characterDtorHook.Dispose();
_copyCharacterHook.Dispose(); _copyCharacterHook.Dispose();
_resourceHandleDestructorHook.Dispose(); _resourceHandleDestructorHook.Dispose();
_characterBaseCreateHook.Dispose();
_characterBaseDestructorHook.Dispose();
_weaponReloadHook.Dispose();
Penumbra.Log.Verbose($"{Prefix} Disposed."); Penumbra.Log.Verbose($"{Prefix} Disposed.");
} }
@ -57,13 +71,12 @@ public unsafe class GameEventManager : IDisposable
} }
public delegate void CharacterDestructorEvent(Character* character); public delegate void CharacterDestructorEvent(Character* character);
public event CharacterDestructorEvent? CharacterDestructor;
#endregion #endregion
#region Copy Character #region Copy Character
private unsafe delegate ulong CopyCharacterDelegate(GameObject* target, GameObject* source, uint unk); private delegate ulong CopyCharacterDelegate(GameObject* target, GameObject* source, uint unk);
[Signature(Sigs.CopyCharacter, DetourName = nameof(CopyCharacterDetour))] [Signature(Sigs.CopyCharacter, DetourName = nameof(CopyCharacterDetour))]
private readonly Hook<CopyCharacterDelegate> _copyCharacterHook = null!; private readonly Hook<CopyCharacterDelegate> _copyCharacterHook = null!;
@ -90,7 +103,6 @@ public unsafe class GameEventManager : IDisposable
} }
public delegate void CopyCharacterEvent(Character* target, Character* source); public delegate void CopyCharacterEvent(Character* target, Character* source);
public event CopyCharacterEvent? CopyCharacter;
#endregion #endregion
@ -122,7 +134,126 @@ public unsafe class GameEventManager : IDisposable
} }
public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle); public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle);
public event ResourceHandleDestructorEvent? ResourceHandleDestructor;
#endregion
#region CharacterBaseCreate
private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d);
[Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))]
private readonly Hook<CharacterBaseCreateDelegate> _characterBaseCreateHook = null!;
private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d)
{
if (CreatingCharacterBase != null)
foreach (var subscriber in CreatingCharacterBase.GetInvocationList())
{
try
{
((CreatingCharacterBaseEvent)subscriber).Invoke(a, b, c);
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
}
}
var ret = _characterBaseCreateHook.Original(a, b, c, d);
if (CharacterBaseCreated != null)
foreach (var subscriber in CharacterBaseCreated.GetInvocationList())
{
try
{
((CharacterBaseCreatedEvent)subscriber).Invoke(a, b, c, ret);
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
}
}
return ret;
}
public delegate void CreatingCharacterBaseEvent(uint modelCharaId, nint customize, nint equipment);
public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject);
#endregion
#region CharacterBase Destructor
public delegate void CharacterBaseDestructorEvent(nint drawBase);
[Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))]
private readonly Hook<CharacterBaseDestructorEvent> _characterBaseDestructorHook = null!;
private void CharacterBaseDestructorDetour(IntPtr drawBase)
{
if (CharacterBaseDestructor != null)
foreach (var subscriber in CharacterBaseDestructor.GetInvocationList())
{
try
{
((CharacterBaseDestructorEvent)subscriber).Invoke(drawBase);
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"{Prefix} Error in {nameof(CharacterBaseDestructorDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
}
}
_characterBaseDestructorHook.Original.Invoke(drawBase);
}
#endregion
#region Weapon Reload
private delegate void WeaponReloadFunc(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7);
[Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))]
private readonly Hook<WeaponReloadFunc> _weaponReloadHook = null!;
private void WeaponReloadDetour(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7)
{
var gameObject = *(nint*)(a1 + 8);
if (WeaponReloading != null)
foreach (var subscriber in WeaponReloading.GetInvocationList())
{
try
{
((WeaponReloadingEvent)subscriber).Invoke(a1, gameObject);
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
}
}
_weaponReloadHook.Original(a1, a2, a3, a4, a5, a6, a7);
if (WeaponReloaded != null)
foreach (var subscriber in WeaponReloaded.GetInvocationList())
{
try
{
((WeaponReloadedEvent)subscriber).Invoke(a1, gameObject);
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
}
}
}
public delegate void WeaponReloadingEvent(nint drawDataContainer, nint gameObject);
public delegate void WeaponReloadedEvent(nint drawDataContainer, nint gameObject);
#endregion #endregion
} }

View file

@ -106,7 +106,11 @@ public class Penumbra : IDalamudPlugin
RedrawService = _tmp.Services.GetRequiredService<RedrawService>(); RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
ResourceService = _tmp.Services.GetRequiredService<ResourceService>(); ResourceService = _tmp.Services.GetRequiredService<ResourceService>();
ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>(); ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>();
PathResolver = _tmp.Services.GetRequiredService<PathResolver>(); using (var t = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
{
PathResolver = _tmp.Services.GetRequiredService<PathResolver>();
}
SetupInterface(); SetupInterface();
SetupApi(); SetupApi();
@ -175,7 +179,6 @@ public class Penumbra : IDalamudPlugin
Config.EnableMods = enabled; Config.EnableMods = enabled;
if (enabled) if (enabled)
{ {
PathResolver.Enable();
if (CharacterUtility.Ready) if (CharacterUtility.Ready)
{ {
CollectionManager.Default.SetFiles(); CollectionManager.Default.SetFiles();
@ -185,7 +188,6 @@ public class Penumbra : IDalamudPlugin
} }
else else
{ {
PathResolver.Disable();
if (CharacterUtility.Ready) if (CharacterUtility.Ready)
{ {
CharacterUtility.ResetAll(); CharacterUtility.ResetAll();

View file

@ -93,11 +93,20 @@ public class PenumbraNew
.AddSingleton<Mod.Manager>() .AddSingleton<Mod.Manager>()
.AddSingleton<ModFileSystem>(); .AddSingleton<ModFileSystem>();
// Add main services // Add Resource services
services.AddSingleton<ResourceLoader>() services.AddSingleton<ResourceLoader>()
.AddSingleton<PathResolver>()
.AddSingleton<CharacterResolver>()
.AddSingleton<ResourceWatcher>(); .AddSingleton<ResourceWatcher>();
// Add Path Resolver
services.AddSingleton<AnimationHookService>()
.AddSingleton<CollectionResolver>()
.AddSingleton<CutsceneService>()
.AddSingleton<DrawObjectState>()
.AddSingleton<MetaState>()
.AddSingleton<PathState>()
.AddSingleton<SubfileHelper>()
.AddSingleton<IdentifiedCollectionCache>()
.AddSingleton<PathResolver>();
// Add Interface // Add Interface
services.AddSingleton<FileDialogService>() services.AddSingleton<FileDialogService>()

View file

@ -29,10 +29,28 @@ public class CommunicatorService : IDisposable
/// </list> </summary> /// </list> </summary>
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModMetaChange = new(nameof(ModMetaChange)); public readonly EventWrapper<ModDataChangeType, Mod, string?> ModMetaChange = new(nameof(ModMetaChange));
/// <summary> <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item>
/// <item>Parameter is the name of the applied collection. </item>
/// <item>Parameter is a pointer to the model id (an uint). </item>
/// <item>Parameter is a pointer to the customize array. </item>
/// <item>Parameter is a pointer to the equip data array. </item>
/// </list> </summary>
public readonly EventWrapper<nint, string, nint, nint, nint> CreatingCharacterBase = new(nameof(CreatingCharacterBase));
/// <summary> <list type="number">
/// <item>Parameter is the game object for which a draw object is created. </item>
/// <item>Parameter is the name of the applied collection. </item>
/// <item>Parameter is the created draw object. </item>
/// </list> </summary>
public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase));
public void Dispose() public void Dispose()
{ {
CollectionChange.Dispose(); CollectionChange.Dispose();
TemporaryGlobalModChange.Dispose(); TemporaryGlobalModChange.Dispose();
ModMetaChange.Dispose(); ModMetaChange.Dispose();
CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose();
} }
} }

View file

@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.Interop;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Widgets; using OtterGui.Widgets;
@ -30,42 +31,54 @@ namespace Penumbra.UI.Tabs;
public class DebugTab : ITab public class DebugTab : ITab
{ {
private readonly StartTracker _timer; private readonly StartTracker _timer;
private readonly PerformanceTracker _performance; private readonly PerformanceTracker _performance;
private readonly Configuration _config; private readonly Configuration _config;
private readonly ModCollection.Manager _collectionManager; private readonly ModCollection.Manager _collectionManager;
private readonly Mod.Manager _modManager; private readonly Mod.Manager _modManager;
private readonly ValidityChecker _validityChecker; private readonly ValidityChecker _validityChecker;
private readonly HttpApi _httpApi; private readonly HttpApi _httpApi;
private readonly PathResolver _pathResolver; private readonly ActorService _actorService;
private readonly ActorService _actorService; private readonly DalamudServices _dalamud;
private readonly DalamudServices _dalamud; private readonly StainService _stains;
private readonly StainService _stains; private readonly CharacterUtility _characterUtility;
private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources;
private readonly ResidentResourceManager _residentResources; private readonly ResourceManagerService _resourceManager;
private readonly ResourceManagerService _resourceManager; private readonly PenumbraIpcProviders _ipc;
private readonly PenumbraIpcProviders _ipc; private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly PathState _pathState;
private readonly SubfileHelper _subfileHelper;
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
private readonly CutsceneService _cutsceneService;
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, PathResolver pathResolver, ActorService actorService, ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService,
DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
ResourceManagerService resourceManager, PenumbraIpcProviders ipc) ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService)
{ {
_timer = timer; _timer = timer;
_performance = performance; _performance = performance;
_config = config; _config = config;
_collectionManager = collectionManager; _collectionManager = collectionManager;
_validityChecker = validityChecker; _validityChecker = validityChecker;
_modManager = modManager; _modManager = modManager;
_httpApi = httpApi; _httpApi = httpApi;
_pathResolver = pathResolver; _actorService = actorService;
_actorService = actorService; _dalamud = dalamud;
_dalamud = dalamud; _stains = stains;
_stains = stains; _characterUtility = characterUtility;
_characterUtility = characterUtility; _residentResources = residentResources;
_residentResources = residentResources; _resourceManager = resourceManager;
_resourceManager = resourceManager; _ipc = ipc;
_ipc = ipc; _collectionResolver = collectionResolver;
_drawObjectState = drawObjectState;
_pathState = pathState;
_subfileHelper = subfileHelper;
_identifiedCollectionCache = identifiedCollectionCache;
_cutsceneService = cutsceneService;
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -131,7 +144,6 @@ public class DebugTab : ITab
PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString());
PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString()); PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString());
PrintValue("Mod Manager Valid", _modManager.Valid.ToString()); PrintValue("Mod Manager Valid", _modManager.Valid.ToString());
PrintValue("Path Resolver Enabled", _pathResolver.Enabled.ToString());
PrintValue("Web Server Enabled", _httpApi.Enabled.ToString()); PrintValue("Web Server Enabled", _httpApi.Enabled.ToString());
} }
@ -200,28 +212,33 @@ public class DebugTab : ITab
return; return;
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Last Game Object: 0x{_pathResolver.LastGameObject:X} ({_pathResolver.LastGameObjectData.ModCollection.Name})"); $"Last Game Object: 0x{_collectionResolver.IdentifyLastGameObjectCollection(true).AssociatedGameObject:X} ({_collectionResolver.IdentifyLastGameObjectCollection(true).ModCollection.Name})");
using (var drawTree = TreeNode("Draw Object to Object")) using (var drawTree = TreeNode("Draw Object to Object"))
{ {
if (drawTree) if (drawTree)
{ {
using var table = Table("###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit); using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit);
if (table) if (table)
foreach (var (ptr, (c, idx)) in _pathResolver.DrawObjectMap) foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState
.OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex)
.ThenBy(kvp => kvp.Value.Item2)
.ThenBy(kvp => kvp.Key))
{ {
var gameObject = (GameObject*)gameObjectPtr;
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(ptr.ToString("X")); ImGui.TextUnformatted($"0x{drawObject:X}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(idx.ToString()); ImGui.TextUnformatted(gameObject->ObjectIndex.ToString());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var obj = (GameObject*)_dalamud.Objects.GetObjectAddress(idx); ImGui.TextUnformatted(child ? "Child" : "Main");
var (address, name) = ImGui.TableNextColumn();
obj != null ? ($"0x{(ulong)obj:X}", new ByteString(obj->Name).ToString()) : ("NULL", "NULL"); var (address, name) = ($"0x{gameObjectPtr:X}", new ByteString(gameObject->Name).ToString());
ImGui.TextUnformatted(address); ImGui.TextUnformatted(address);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(name); ImGui.TextUnformatted(name);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(c.ModCollection.Name); var collection = _collectionResolver.IdentifyCollection(gameObject, true);
ImGui.TextUnformatted(collection.ModCollection.Name);
} }
} }
} }
@ -230,16 +247,14 @@ public class DebugTab : ITab
{ {
if (pathTree) if (pathTree)
{ {
using var table = Table("###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); using var table = Table("###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table) if (table)
foreach (var (path, collection) in _pathResolver.PathCollections) foreach (var data in _pathState.CurrentData)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); ImGui.TextUnformatted($"{data.AssociatedGameObject:X}");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TextUnformatted(collection.ModCollection.Name); ImGui.TextUnformatted(data.ModCollection.Name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(collection.AssociatedGameObject.ToString("X"));
} }
} }
} }
@ -248,26 +263,30 @@ public class DebugTab : ITab
{ {
if (resourceTree) if (resourceTree)
{ {
using var table = Table("###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); using var table = Table("###ResourceCollectionResolverTable", 4, ImGuiTableFlags.SizingFixedFit);
if (table) if (table)
{ {
ImGuiUtil.DrawTableColumn("Current Mtrl Data"); ImGuiUtil.DrawTableColumn("Current Mtrl Data");
ImGuiUtil.DrawTableColumn(_pathResolver.CurrentMtrlData.ModCollection.Name); ImGuiUtil.DrawTableColumn(_subfileHelper.MtrlData.ModCollection.Name);
ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentMtrlData.AssociatedGameObject:X}"); ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.MtrlData.AssociatedGameObject:X}");
ImGuiUtil.DrawTableColumn("Current Avfx Data");
ImGuiUtil.DrawTableColumn(_pathResolver.CurrentAvfxData.ModCollection.Name);
ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentAvfxData.AssociatedGameObject:X}");
ImGuiUtil.DrawTableColumn("Current Resources");
ImGuiUtil.DrawTableColumn(_pathResolver.SubfileCount.ToString());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
foreach (var (resource, resolve) in _pathResolver.ResourceCollections) ImGuiUtil.DrawTableColumn("Current Avfx Data");
ImGuiUtil.DrawTableColumn(_subfileHelper.AvfxData.ModCollection.Name);
ImGuiUtil.DrawTableColumn($"0x{_subfileHelper.AvfxData.AssociatedGameObject:X}");
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("Current Resources");
ImGuiUtil.DrawTableColumn(_subfileHelper.Count.ToString());
ImGui.TableNextColumn();
ImGui.TableNextColumn();
foreach (var (resource, resolve) in _subfileHelper)
{ {
ImGuiUtil.DrawTableColumn($"0x{resource:X}"); ImGuiUtil.DrawTableColumn($"0x{resource:X}");
ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name); ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name);
ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}"); ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}");
ImGuiUtil.DrawTableColumn($"{((ResourceHandle*)resource)->FileName()}");
} }
} }
} }
@ -277,10 +296,12 @@ public class DebugTab : ITab
{ {
if (identifiedTree) if (identifiedTree)
{ {
using var table = Table("##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit); using var table = Table("##PathCollectionsIdentifiedTable", 4, ImGuiTableFlags.SizingFixedFit);
if (table) if (table)
foreach (var (address, identifier, collection) in PathResolver.IdentifiedCache) foreach (var (address, identifier, collection) in _identifiedCollectionCache
.OrderBy(kvp => ((GameObject*)kvp.Address)->ObjectIndex))
{ {
ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}");
ImGuiUtil.DrawTableColumn($"0x{address:X}"); ImGuiUtil.DrawTableColumn($"0x{address:X}");
ImGuiUtil.DrawTableColumn(identifier.ToString()); ImGuiUtil.DrawTableColumn(identifier.ToString());
ImGuiUtil.DrawTableColumn(collection.Name); ImGuiUtil.DrawTableColumn(collection.Name);
@ -294,7 +315,7 @@ public class DebugTab : ITab
{ {
using var table = Table("###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit); using var table = Table("###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit);
if (table) if (table)
foreach (var (idx, actor) in _pathResolver.CutsceneActors) foreach (var (idx, actor) in _cutsceneService.Actors)
{ {
ImGuiUtil.DrawTableColumn($"Cutscene Actor {idx}"); ImGuiUtil.DrawTableColumn($"Cutscene Actor {idx}");
ImGuiUtil.DrawTableColumn(actor.Name.ToString()); ImGuiUtil.DrawTableColumn(actor.Name.ToString());